diff --git a/PROJECT_ARCHITECTURE.md b/PROJECT_ARCHITECTURE.md deleted file mode 100644 index 00ee9672..00000000 --- a/PROJECT_ARCHITECTURE.md +++ /dev/null @@ -1,1768 +0,0 @@ - -You are a senior Flutter developer. You will be given this entire document as your specification. -Your task: generate a complete, compilable Flutter project from scratch following every instruction below. -Do NOT skip any file. Do NOT summarize. Generate FULL, working Dart code for every file listed. -When you see placeholder names like `{{APP_NAME}}`, `{{PACKAGE_NAME}}`, `{{BASE_URL}}`, replace them with the values the user provides. If no values are provided, use the defaults in Section 1. - - -# Flutter Project Generator — Architecture Specification - -> **What this document is:** A precise, machine-readable specification for an AI to generate a greenfield Flutter project. Every file path, every class, every method signature, and every pattern is defined below with copy-ready code. Follow it top-to-bottom. - ---- - -## 1. DEFAULTS — Replace These for Your Project - -``` -APP_NAME = "My App" -DART_PACKAGE = "my_app" -ORG_IDENTIFIER = "com.example.myapp" -BASE_URL = "https://api.example.com/" -PRIMARY_COLOR = 0xFFED1C2B (Red) -FIGMA_WIDTH = 430 -FIGMA_HEIGHT = 927 -SUPPORTED_LOCALES = [en-US, ar-SA] -PRIMARY_FONT = "Poppins" -SECONDARY_FONT = "GESSTwo" (Arabic font) -``` - -When the user asks you to generate this project, first ask them for these values. If they say "use defaults," use the values above. - ---- - -## 2. COMMAND — Create the Flutter Project - -Run this first: -```bash -flutter create --org {{ORG_IDENTIFIER}} --project-name {{DART_PACKAGE}} {{DART_PACKAGE}} -cd {{DART_PACKAGE}} -``` - ---- - -## 3. FILE: `pubspec.yaml` - -Generate this file EXACTLY. Do not add or remove packages. - -```yaml -name: {{DART_PACKAGE}} -description: "{{APP_NAME}}" -publish_to: 'none' -version: 1.0.0+1 - -environment: - sdk: ">=3.6.0 <4.0.0" - -dependencies: - flutter: - sdk: flutter - flutter_localizations: - sdk: flutter - - # === Core Architecture === - provider: ^6.1.5+1 - get_it: ^8.2.0 - dartz: ^0.10.1 - equatable: ^2.0.7 - - # === Networking === - http: ^1.5.0 - connectivity_plus: ^6.1.5 - - # === Firebase === - firebase_core: any - firebase_messaging: ^15.2.10 - firebase_analytics: ^11.5.1 - firebase_crashlytics: ^4.3.8 - - # === UI === - cupertino_icons: ^1.0.8 - flutter_svg: ^2.2.0 - cached_network_image: ^3.4.1 - auto_size_text: ^3.0.0 - shimmer: ^3.0.0 - sizer: ^3.1.3 - lottie: ^3.3.1 - smooth_corner: ^1.1.1 - flutter_staggered_animations: ^1.1.1 - fl_chart: 1.0.0 - flutter_rating_bar: ^4.0.1 - - # === Localization === - easy_localization: ^3.0.8 - intl: ^0.20.2 - - # === Storage === - shared_preferences: ^2.5.3 - path_provider: ^2.0.8 - - # === Device === - permission_handler: ^12.0.1 - local_auth: ^2.3.0 - device_info_plus: ^11.5.0 - image_picker: ^1.2.0 - url_launcher: ^6.3.2 - share_plus: ^11.1.0 - - # === Notifications === - flutter_local_notifications: ^19.4.1 - timezone: ^0.10.0 - fluttertoast: ^8.2.12 - - # === Logging === - logger: ^2.6.1 - -dev_dependencies: - flutter_test: - sdk: flutter - flutter_lints: ^5.0.0 - -flutter: - uses-material-design: true - assets: - - assets/ - - assets/json/ - - assets/fonts/ - - assets/langs/ - - assets/images/ - - assets/images/svg/ - - assets/images/png/ - - assets/animations/ - - assets/animations/lottie/ - - fonts: - - family: Poppins - fonts: - - asset: assets/fonts/poppins/Poppins-SemiBold.ttf - weight: 600 - - asset: assets/fonts/poppins/Poppins-Medium.ttf - weight: 500 - - asset: assets/fonts/poppins/Poppins-Regular.ttf - weight: 400 - - asset: assets/fonts/poppins/Poppins-Light.ttf - weight: 300 -``` - -> **Note:** If the user provides a SECONDARY_FONT, add its font family block here too. - ---- - -## 4. FILE: `analysis_options.yaml` - -```yaml -include: package:flutter_lints/flutter.yaml - -linter: - rules: - # prefer_single_quotes: true -``` - ---- - -## 5. FOLDER STRUCTURE — Create ALL These Directories - -```bash -mkdir -p lib/core/api -mkdir -p lib/core/common_models -mkdir -p lib/core/exceptions -mkdir -p lib/core/utils -mkdir -p lib/services/analytics -mkdir -p lib/features -mkdir -p lib/presentation/home/widgets -mkdir -p lib/presentation/authentication -mkdir -p lib/presentation/onboarding -mkdir -p lib/routes -mkdir -p lib/theme -mkdir -p lib/extensions -mkdir -p lib/widgets/buttons -mkdir -p lib/widgets/loader -mkdir -p lib/widgets/bottomsheet -mkdir -p lib/widgets/bottom_navigation -mkdir -p lib/widgets/shimmer -mkdir -p lib/widgets/routes -mkdir -p lib/generated -mkdir -p assets/fonts/poppins -mkdir -p assets/images/svg -mkdir -p assets/images/png -mkdir -p assets/animations/lottie -mkdir -p assets/json -mkdir -p assets/langs -mkdir -p assets/sounds -``` - ---- - -## 6. CORE LAYER — Generate Each File Exactly - -### 6.1 FILE: `lib/core/enums.dart` - -```dart -enum ViewStateEnum { hide, idle, busy, error, busyLocal, errorLocal } - -enum AppEnvironmentTypeEnum { dev, uat, preProd, qa, staging, prod } - -enum GenderTypeEnum { male, female } - -enum ChipTypeEnum { success, error, alert, info, warning, lightBg, primaryRed } - -enum LoginTypeEnum { sms, whatsapp, face, fingerprint } - -enum OTPTypeEnum { sms, whatsapp, faceIDFingerprint } -``` - -### 6.2 FILE: `lib/core/exceptions/api_failure.dart` - -```dart -import 'package:equatable/equatable.dart'; - -abstract class Failure extends Equatable implements Exception { - final String message; - const Failure(this.message); -} - -class ServerFailure extends Failure { - final String url; - const ServerFailure(super.message, {this.url = ""}); - @override - List get props => [message]; -} - -class DataParsingFailure extends Failure { - const DataParsingFailure(super.message); - @override - List get props => [message]; -} - -class ConnectivityFailure extends Failure { - const ConnectivityFailure(super.message); - @override - List get props => [message]; -} - -class UnAuthenticatedUserFailure extends Failure { - final String url; - const UnAuthenticatedUserFailure(super.message, {this.url = ""}); - @override - List get props => [message]; -} - -class AppUpdateFailure extends Failure { - const AppUpdateFailure(super.message); - @override - List get props => [message]; -} - -class StatusCodeFailure extends Failure { - const StatusCodeFailure(super.message); - @override - List get props => [message]; -} - -class UnknownFailure extends Failure { - final String url; - const UnknownFailure(super.message, {this.url = ""}); - @override - List get props => [message]; -} - -class UserIntimationFailure extends Failure { - const UserIntimationFailure(super.message); - @override - List get props => [message]; -} - -class MessageStatusFailure extends Failure { - const MessageStatusFailure(super.message); - @override - List get props => [message]; -} - -class InvalidCredentials extends Failure { - const InvalidCredentials(String? message) : super(message ?? ''); - @override - List get props => [message]; -} - -class LocalStorageFailure extends Failure { - const LocalStorageFailure(super.message); - @override - List get props => [message]; -} -``` - -### 6.3 FILE: `lib/core/common_models/generic_api_model.dart` - -```dart -class GenericApiModel { - final int? messageStatus; - final String? errorMessage; - final int? statusCode; - final T? data; - - GenericApiModel({this.messageStatus, this.errorMessage, this.statusCode, this.data}); - - factory GenericApiModel.fromJson( - Map json, - T Function(Object? json)? fromJsonT, - ) { - return GenericApiModel( - messageStatus: json['messageStatus'] as int?, - errorMessage: json['errorMessage'] as String?, - statusCode: json['statusCode'] as int?, - data: fromJsonT != null ? fromJsonT(json['data']) : json['data'] as T?, - ); - } - - Map toJson(Object Function(T value)? toJsonT) { - return { - 'messageStatus': messageStatus, - 'errorMessage': errorMessage, - 'statusCode': statusCode, - 'data': toJsonT != null && data != null ? toJsonT(data as T) : data, - }; - } -} -``` - -### 6.4 FILE: `lib/core/api_consts.dart` - -```dart -import 'package:{{DART_PACKAGE}}/core/enums.dart'; - -const int VERSION_ID = 1; -const int CHANNEL = 3; -const String IP_ADDRESS = "10.20.10.20"; -const String GENERAL_ID = "Aborkan"; -const int PATIENT_TYPE = 1; -const int PATIENT_TYPE_ID = 1; -const String SETUP_ID = "010266"; -const bool IS_DENTAL_ALLOWED_BACKEND = false; - -class ApiConsts { - static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.prod; - static String baseUrl = '{{BASE_URL}}'; - - static int appVersionID = VERSION_ID; - static int appChannelId = CHANNEL; - static String appIpAddress = IP_ADDRESS; - static String appGeneralId = GENERAL_ID; - static String sessionID = ""; - - static void setBackendURLs() { - switch (appEnvironmentType) { - case AppEnvironmentTypeEnum.prod: - baseUrl = '{{BASE_URL}}'; - break; - case AppEnvironmentTypeEnum.dev: - case AppEnvironmentTypeEnum.uat: - case AppEnvironmentTypeEnum.preProd: - case AppEnvironmentTypeEnum.qa: - case AppEnvironmentTypeEnum.staging: - baseUrl = '{{BASE_URL}}'; - break; - } - } - - // ── Add endpoint paths here as you build features ── - // static const String login = "api/auth/login"; -} -``` - -### 6.5 FILE: `lib/core/cache_consts.dart` - -```dart -class CacheConst { - static const String firstLaunch = "firstLaunch"; - static const String appAuthToken = "app_auth_token"; - static const String loggedInUserObj = "logged_in_user_obj"; - static const String pushToken = "push_token"; - static const String appLanguage = 'language'; - static const String isDarkMode = 'is_dark_mode'; - - // Add more cache keys as features are built -} -``` - -### 6.6 FILE: `lib/core/app_state.dart` - -```dart -import 'dart:io'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:{{DART_PACKAGE}}/services/navigation_service.dart'; - -class AppState { - final NavigationService navigationService; - - AppState({required this.navigationService}); - - double userLat = 0.0; - double userLong = 0.0; - - bool isArabic() { - final ctx = navigationService.navigatorKey.currentContext; - if (ctx == null) return false; - return EasyLocalization.of(ctx)?.locale.languageCode == "ar"; - } - - int getLanguageID() => isArabic() ? 1 : 2; - String? getLanguageCode() { - final ctx = navigationService.navigatorKey.currentContext; - if (ctx == null) return "en"; - return EasyLocalization.of(ctx)?.locale.languageCode; - } - int getDeviceTypeID() => Platform.isIOS ? 1 : 2; - - bool isAuthenticated = false; - String appAuthToken = ""; - String deviceToken = ""; - String voipToken = ""; - String sessionId = ""; - String deviceTypeID = ""; - - Map? authenticatedUser; - - void setAuthenticatedUser(Map? user) { - authenticatedUser = user; - isAuthenticated = user != null; - } - - void resetLocation() { - userLat = 0.0; - userLong = 0.0; - } -} -``` - -### 6.7 FILE: `lib/core/app_assets.dart` - -```dart -class AppAssets { - static const String svgBasePath = 'assets/images/svg'; - static const String pngBasePath = 'assets/images/png'; - static const String lottieBasePath = 'assets/animations/lottie'; - - // Add asset constants here as you add files. Examples: - // static const String logo = '$svgBasePath/logo.svg'; - // static const String placeholder = '$pngBasePath/placeholder.png'; -} -``` - -### 6.8 FILE: `lib/core/app_export.dart` - -```dart -export '../routes/app_routes.dart'; -export 'utils/size_utils.dart'; -``` - -### 6.9 FILE: `lib/core/utils/size_utils.dart` - -```dart -import 'package:flutter/material.dart'; - -const num figmaDesignWidth = {{FIGMA_WIDTH}}; -const num figmaDesignHeight = {{FIGMA_HEIGHT}}; - -class SizeUtils { - static late double width; - static late double height; - static late DeviceType deviceType; - - static void init(BoxConstraints constraints, Orientation orientation) { - width = constraints.maxWidth; - height = constraints.maxHeight; - deviceType = width > 600 ? DeviceType.tablet : DeviceType.phone; - } -} - -enum DeviceType { phone, tablet } - -bool get isTablet => SizeUtils.deviceType == DeviceType.tablet; - -extension ResponsiveExtension on num { - double get _width => SizeUtils.width; - double get _height => SizeUtils.height; - - double get f { - double scale = (_width < _height ? _width : _height) / figmaDesignWidth; - double clamp = isTablet ? 1.4 : 1.2; - if (scale > clamp) scale = clamp; - return this * scale; - } - - double get w => (this * _width) / figmaDesignWidth; - double get h => (this * _height) / figmaDesignHeight; - double get r => (this * _width) / figmaDesignWidth; -} -``` - -### 6.10 FILE: `lib/core/utils/loading_utils.dart` - -```dart -import 'package:flutter/material.dart'; -import 'package:{{DART_PACKAGE}}/core/dependencies.dart'; -import 'package:{{DART_PACKAGE}}/services/navigation_service.dart'; - -class LoadingUtils { - static bool isLoading = false; - static OverlayEntry? _overlayEntry; - - static void showFullScreenLoader() { - final context = getIt.get().navigatorKey.currentContext; - if (context == null || isLoading) return; - isLoading = true; - _overlayEntry = OverlayEntry( - builder: (_) => Container( - color: Colors.black38, - child: const Center(child: CircularProgressIndicator()), - ), - ); - Overlay.of(context).insert(_overlayEntry!); - } - - static void hideFullScreenLoader() { - _overlayEntry?.remove(); - _overlayEntry = null; - isLoading = false; - } -} -``` - -### 6.11 FILE: `lib/core/api/api_client.dart` - -```dart -import 'dart:convert'; -import 'package:flutter/foundation.dart'; -import 'package:http/http.dart' as http; -import 'package:{{DART_PACKAGE}}/core/api_consts.dart'; -import 'package:{{DART_PACKAGE}}/core/app_state.dart'; -import 'package:{{DART_PACKAGE}}/core/exceptions/api_failure.dart'; - -abstract class ApiClient { - Future post( - String endPoint, { - required Map body, - required Function(dynamic response, int statusCode, {int? messageStatus, String? errorMessage}) onSuccess, - required Function(String error, int statusCode, {int? messageStatus, Failure? failureType}) onFailure, - bool isExternal = false, - Map? apiHeaders, - }); - - Future get( - String endPoint, { - required Function(dynamic response, int statusCode) onSuccess, - required Function(String error, int statusCode) onFailure, - Map? queryParams, - Map? apiHeaders, - bool isExternal = false, - }); -} - -class ApiClientImp implements ApiClient { - final AppState _appState; - - ApiClientImp({required AppState appState}) : _appState = appState; - - Map _defaultHeaders() => {'Content-Type': 'application/json', 'Accept': 'application/json'}; - - void _injectCommonFields(Map body) { - body['VersionID'] = ApiConsts.appVersionID.toString(); - body['Channel'] = ApiConsts.appChannelId.toString(); - body['IPAdress'] = ApiConsts.appIpAddress; - body['generalid'] = ApiConsts.appGeneralId; - body['LanguageID'] = _appState.getLanguageID().toString(); - body['Latitude'] = _appState.userLat.toString(); - body['Longitude'] = _appState.userLong.toString(); - body['DeviceTypeID'] = _appState.deviceTypeID; - if (_appState.appAuthToken.isNotEmpty) { - body[_appState.isAuthenticated ? 'TokenID' : 'LogInTokenID'] = _appState.appAuthToken; - } - } - - @override - Future post( - String endPoint, { - required Map body, - required Function(dynamic response, int statusCode, {int? messageStatus, String? errorMessage}) onSuccess, - required Function(String error, int statusCode, {int? messageStatus, Failure? failureType}) onFailure, - bool isExternal = false, - Map? apiHeaders, - }) async { - final url = isExternal ? endPoint : ApiConsts.baseUrl + endPoint; - final headers = apiHeaders ?? _defaultHeaders(); - if (!isExternal) _injectCommonFields(body); - - try { - final response = await http.post(Uri.parse(url), headers: headers, body: jsonEncode(body)); - final statusCode = response.statusCode; - - if (statusCode == 401) { - onFailure('Unauthenticated', statusCode, failureType: UnAuthenticatedUserFailure('Session expired', url: url)); - return; - } - if (statusCode == 426) { - onFailure('App update required', statusCode, failureType: const AppUpdateFailure('Please update the app')); - return; - } - - if (statusCode >= 200 && statusCode < 300) { - final decoded = jsonDecode(response.body); - final int? messageStatus = decoded['MessageStatus']; - final String? errorMessage = decoded['ErrorEndUserMessage'] ?? decoded['ErrorMessage']; - - if (messageStatus == 1 || messageStatus == null) { - onSuccess(decoded, statusCode, messageStatus: messageStatus, errorMessage: errorMessage); - } else { - onFailure(errorMessage ?? 'Request failed', statusCode, - messageStatus: messageStatus, - failureType: MessageStatusFailure(errorMessage ?? 'Request failed with status $messageStatus')); - } - } else { - onFailure('Server error: $statusCode', statusCode, failureType: ServerFailure('Server error: $statusCode', url: url)); - } - } catch (e) { - if (kDebugMode) print('API POST error ($url): $e'); - onFailure(e.toString(), 0, failureType: UnknownFailure(e.toString(), url: url)); - } - } - - @override - Future get( - String endPoint, { - required Function(dynamic response, int statusCode) onSuccess, - required Function(String error, int statusCode) onFailure, - Map? queryParams, - Map? apiHeaders, - bool isExternal = false, - }) async { - final baseUrl = isExternal ? endPoint : ApiConsts.baseUrl + endPoint; - final uri = Uri.parse(baseUrl).replace(queryParameters: queryParams?.map((k, v) => MapEntry(k, v.toString()))); - final headers = apiHeaders ?? _defaultHeaders(); - - try { - final response = await http.get(uri, headers: headers); - if (response.statusCode >= 200 && response.statusCode < 300) { - onSuccess(jsonDecode(response.body), response.statusCode); - } else { - onFailure('GET failed: ${response.statusCode}', response.statusCode); - } - } catch (e) { - onFailure(e.toString(), 0); - } - } -} -``` - ---- - -## 7. SERVICES LAYER — Generate Each File Exactly - -### 7.1 FILE: `lib/services/logger_service.dart` - -```dart -import 'package:logger/logger.dart'; - -abstract class LoggerService { - void logError(String message); - void logInfo(String message); -} - -class LoggerServiceImp implements LoggerService { - final Logger logger; - LoggerServiceImp({required this.logger}); - - @override - void logError(String message) => logger.e(message); - @override - void logInfo(String message) => logger.d(message); -} -``` - -### 7.2 FILE: `lib/services/navigation_service.dart` - -```dart -import 'package:flutter/material.dart'; - -class NavigationService { - final GlobalKey navigatorKey = GlobalKey(); - - BuildContext? get context => navigatorKey.currentContext; - - Future push(Route route) => navigatorKey.currentState!.push(route); - - Future pushAndRemoveUntil(Route route, RoutePredicate predicate) => - navigatorKey.currentState!.pushAndRemoveUntil(route, predicate); - - void pop([T? result]) => navigatorKey.currentState!.pop(result); - - void popUntilNamed(String routeName) => - navigatorKey.currentState?.popUntil(ModalRoute.withName(routeName)); - - void replaceAllRoutesAndNavigateTo(String routeName) { - navigatorKey.currentState?.pushNamedAndRemoveUntil(routeName, (route) => false); - } - - void pushAndReplace(String routeName) => - navigatorKey.currentState?.pushReplacementNamed(routeName); - - void pushNamed(String routeName, {Object? arguments}) => - navigatorKey.currentState?.pushNamed(routeName, arguments: arguments); - - Future pushPage({required Widget page, bool fullscreenDialog = false}) => - navigatorKey.currentState!.push( - MaterialPageRoute(builder: (_) => page, fullscreenDialog: fullscreenDialog), - ); -} -``` - -### 7.3 FILE: `lib/services/cache_service.dart` - -```dart -import 'dart:convert'; -import 'package:{{DART_PACKAGE}}/services/logger_service.dart'; -import 'package:shared_preferences/shared_preferences.dart'; - -abstract class CacheService { - Future saveString({required String key, required String value}); - Future saveInt({required String key, required int value}); - Future saveBool({required String key, required bool value}); - String? getString({required String key}); - int? getInt({required String key}); - bool? getBool({required String key}); - Future getObject({required String key}); - Future saveObject({required String key, required dynamic value}); - Future remove({required String key}); - Future clear(); -} - -class CacheServiceImp implements CacheService { - final SharedPreferences sharedPreferences; - final LoggerService loggerService; - - CacheServiceImp({required this.sharedPreferences, required this.loggerService}); - - @override - Future saveString({required String key, required String value}) async => await sharedPreferences.setString(key, value); - @override - Future saveInt({required String key, required int value}) async => await sharedPreferences.setInt(key, value); - @override - Future saveBool({required String key, required bool value}) async => await sharedPreferences.setBool(key, value); - @override - String? getString({required String key}) => sharedPreferences.getString(key); - @override - int? getInt({required String key}) => sharedPreferences.getInt(key); - @override - bool? getBool({required String key}) => sharedPreferences.getBool(key); - - @override - Future getObject({required String key}) async { - try { - await sharedPreferences.reload(); - final s = sharedPreferences.getString(key); - return s == null ? null : json.decode(s); - } catch (e) { - loggerService.logError(e.toString()); - return null; - } - } - - @override - Future saveObject({required String key, required dynamic value}) async => - await sharedPreferences.setString(key, json.encode(value)); - @override - Future remove({required String key}) async => await sharedPreferences.remove(key); - @override - Future clear() async => await sharedPreferences.clear(); -} -``` - -### 7.4 FILE: `lib/services/dialog_service.dart` - -```dart -import 'package:flutter/material.dart'; -import 'package:{{DART_PACKAGE}}/services/navigation_service.dart'; -import 'package:{{DART_PACKAGE}}/theme/colors.dart'; - -abstract class DialogService { - Future showErrorBottomSheet({String title, required String message, Function()? onOkPressed}); - Future showConfirmBottomSheet({required String message, required Function() onOkPressed, Function()? onCancelPressed}); -} - -class DialogServiceImp implements DialogService { - final NavigationService navigationService; - bool _isSheetShowing = false; - - DialogServiceImp({required this.navigationService}); - - @override - Future showErrorBottomSheet({String title = "", required String message, Function()? onOkPressed}) async { - if (_isSheetShowing) return; - final context = navigationService.navigatorKey.currentContext; - if (context == null) return; - _isSheetShowing = true; - await showModalBottomSheet( - context: context, - shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(16))), - builder: (_) => Padding( - padding: const EdgeInsets.all(24), - child: Column(mainAxisSize: MainAxisSize.min, children: [ - if (title.isNotEmpty) ...[Text(title, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600)), const SizedBox(height: 12)], - Text(message, textAlign: TextAlign.center), - const SizedBox(height: 24), - SizedBox( - width: double.infinity, - child: ElevatedButton( - style: ElevatedButton.styleFrom(backgroundColor: AppColors.primaryRedColor), - onPressed: () { Navigator.pop(context); onOkPressed?.call(); }, - child: const Text('OK', style: TextStyle(color: Colors.white)), - ), - ), - ]), - ), - ); - _isSheetShowing = false; - } - - @override - Future showConfirmBottomSheet({required String message, required Function() onOkPressed, Function()? onCancelPressed}) async { - final context = navigationService.navigatorKey.currentContext; - if (context == null) return; - await showModalBottomSheet( - context: context, - shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(16))), - builder: (_) => Padding( - padding: const EdgeInsets.all(24), - child: Column(mainAxisSize: MainAxisSize.min, children: [ - Text(message, textAlign: TextAlign.center), - const SizedBox(height: 24), - Row(children: [ - Expanded(child: OutlinedButton(onPressed: () { Navigator.pop(context); onCancelPressed?.call(); }, child: const Text('Cancel'))), - const SizedBox(width: 12), - Expanded(child: ElevatedButton( - style: ElevatedButton.styleFrom(backgroundColor: AppColors.primaryRedColor), - onPressed: () { Navigator.pop(context); onOkPressed(); }, - child: const Text('OK', style: TextStyle(color: Colors.white)), - )), - ]), - ]), - ), - ); - } -} -``` - -### 7.5 FILE: `lib/services/error_handler_service.dart` - -```dart -import 'package:{{DART_PACKAGE}}/core/exceptions/api_failure.dart'; -import 'package:{{DART_PACKAGE}}/core/utils/loading_utils.dart'; -import 'package:{{DART_PACKAGE}}/services/dialog_service.dart'; -import 'package:{{DART_PACKAGE}}/services/logger_service.dart'; -import 'package:{{DART_PACKAGE}}/services/navigation_service.dart'; -import 'package:{{DART_PACKAGE}}/routes/app_routes.dart'; - -abstract class ErrorHandlerService { - Future handleError({required Failure failure, Function()? onOkPressed, Function(Failure)? onUnHandledFailure, Function(Failure)? onMessageStatusFailure}); -} - -class ErrorHandlerServiceImp implements ErrorHandlerService { - final DialogService dialogService; - final LoggerService loggerService; - final NavigationService navigationService; - - ErrorHandlerServiceImp({required this.dialogService, required this.loggerService, required this.navigationService}); - - @override - Future handleError({required Failure failure, Function()? onOkPressed, Function(Failure)? onUnHandledFailure, Function(Failure)? onMessageStatusFailure}) async { - if (LoadingUtils.isLoading) LoadingUtils.hideFullScreenLoader(); - - if (failure is ServerFailure) { - loggerService.logError("Server: ${failure.message}"); - await _show(failure); - } else if (failure is ConnectivityFailure) { - loggerService.logError("Connectivity: ${failure.message}"); - await _show(failure, title: "No Internet"); - } else if (failure is UnAuthenticatedUserFailure) { - loggerService.logError("Unauth: ${failure.message}"); - await _show(failure, title: "Session Expired", onOk: () => navigationService.replaceAllRoutesAndNavigateTo(AppRoutes.landingScreen)); - } else if (failure is AppUpdateFailure) { - await _show(failure, title: "Update Required"); - } else if (failure is DataParsingFailure) { - loggerService.logError("Parse: ${failure.message}"); - await _show(failure, title: "Data Error"); - } else if (failure is UserIntimationFailure) { - onUnHandledFailure != null ? onUnHandledFailure(failure) : await _show(failure, onOk: onOkPressed); - } else if (failure is MessageStatusFailure) { - onMessageStatusFailure != null ? onMessageStatusFailure(failure) : await _show(failure, onOk: onOkPressed); - } else { - loggerService.logError("Unhandled: $failure"); - await _show(failure, onOk: onOkPressed); - } - } - - Future _show(Failure f, {String? title, Function()? onOk}) async => - await dialogService.showErrorBottomSheet(title: title ?? "", message: f.message, onOkPressed: onOk); -} -``` - -### 7.6 FILE: `lib/services/firebase_service.dart` - -```dart -import 'package:firebase_messaging/firebase_messaging.dart'; -import 'package:{{DART_PACKAGE}}/core/app_state.dart'; -import 'package:{{DART_PACKAGE}}/services/logger_service.dart'; - -abstract class FirebaseService { - Future getDeviceToken(); -} - -class FirebaseServiceImpl implements FirebaseService { - final FirebaseMessaging firebaseMessaging; - final LoggerService loggerService; - final AppState appState; - - FirebaseServiceImpl({required this.firebaseMessaging, required this.loggerService, required this.appState}); - - @override - Future getDeviceToken() async { - try { - final token = await firebaseMessaging.getToken(); - appState.deviceToken = token ?? ""; - return appState.deviceToken; - } catch (e) { - loggerService.logError(e.toString()); - return ""; - } - } -} -``` - -### 7.7 FILE: `lib/services/notification_service.dart` - -```dart -import 'package:flutter_local_notifications/flutter_local_notifications.dart'; -import 'package:{{DART_PACKAGE}}/services/logger_service.dart'; -import 'package:timezone/data/latest_all.dart' as tz; - -abstract class NotificationService { - Future initialize({Function(String payload)? onNotificationClick}); - Future showNotification({required String title, required String body, String? payload}); - Future cancelAllNotifications(); -} - -class NotificationServiceImp implements NotificationService { - final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin; - final LoggerService loggerService; - - NotificationServiceImp({required this.flutterLocalNotificationsPlugin, required this.loggerService}); - - @override - Future initialize({Function(String payload)? onNotificationClick}) async { - tz.initializeTimeZones(); - const android = AndroidInitializationSettings('app_icon'); - const ios = DarwinInitializationSettings(requestAlertPermission: true, requestBadgePermission: true, requestSoundPermission: true); - await flutterLocalNotificationsPlugin.initialize( - const InitializationSettings(android: android, iOS: ios), - onDidReceiveNotificationResponse: (r) => onNotificationClick?.call(r.payload ?? ''), - ); - } - - @override - Future showNotification({required String title, required String body, String? payload}) async { - const details = NotificationDetails( - android: AndroidNotificationDetails('general', 'General', importance: Importance.high, priority: Priority.high), - iOS: DarwinNotificationDetails(), - ); - await flutterLocalNotificationsPlugin.show(DateTime.now().millisecondsSinceEpoch ~/ 1000, title, body, details, payload: payload); - } - - @override - Future cancelAllNotifications() async => await flutterLocalNotificationsPlugin.cancelAll(); -} -``` - -### 7.8 FILE: `lib/services/analytics/analytics_service.dart` - -```dart -import 'package:firebase_analytics/firebase_analytics.dart'; - -class GAnalytics { - final _analytics = FirebaseAnalytics.instance; - - Future logEvent(String name, {Map? parameters}) async { - try { - await _analytics.logEvent(name: name.trim().toLowerCase(), parameters: parameters?.map((k, v) => MapEntry(k, v as Object))); - } catch (_) {} - } - - Future setUser({String? userId, String? language}) async { - if (userId != null) _analytics.setUserProperty(name: 'userid', value: userId); - if (language != null) _analytics.setUserProperty(name: 'user_language', value: language); - } -} -``` - ---- - -## 8. THEME LAYER - -### 8.1 FILE: `lib/theme/colors.dart` - -```dart -import 'package:flutter/material.dart'; - -class AppColors { - static bool isDarkMode = false; - static const transparent = Colors.transparent; - - static Color get scaffoldBgColor => isDarkMode ? dark.scaffoldBgColor : const Color(0xFFF8F8F8); - static Color get whiteColor => isDarkMode ? dark.whiteColor : const Color(0xFFFFFFFF); - static Color get primaryRedColor => isDarkMode ? dark.primaryRedColor : const Color({{PRIMARY_COLOR}}); - static Color get secondaryLightRedColor => isDarkMode ? dark.secondaryLightRedColor : const Color(0xFFFEE9EA); - static const Color successColor = Color(0xFF18C273); - static Color get textColor => isDarkMode ? dark.textColor : const Color(0xFF2E3039); - static Color get iconColor => isDarkMode ? dark.iconColor : const Color(0xFF2E3039); - static Color get greyColor => isDarkMode ? dark.greyColor : const Color(0xFFEFEFF0); - static Color get textColorLight => isDarkMode ? dark.textColorLight : const Color(0xFF5E5E5E); - static Color get dividerColor => isDarkMode ? dark.dividerColor : const Color(0x40D2D2D2); - static Color get borderGrayColor => isDarkMode ? dark.borderGrayColor : const Color(0x332E3039); - static Color get greyTextColor => isDarkMode ? dark.greyTextColor : const Color(0xFF8F9AA3); - static Color get inputLabelTextColor => isDarkMode ? dark.inputLabelTextColor : const Color(0xFF898A8D); - static Color get errorColor => isDarkMode ? dark.errorColor : const Color(0xFFED1C2B); - static Color get warningColor => isDarkMode ? dark.warningColor : const Color(0xFFFFCC00); - static Color get infoColor => isDarkMode ? dark.infoColor : const Color(0xFF0B85F7); - static Color get shimmerBaseColor => isDarkMode ? dark.shimmerBaseColor : const Color(0xFFE0E0E0); - static Color get shimmerHighlightColor => isDarkMode ? dark.shimmerHighlightColor : const Color(0xFFF5F5F5); - - static Color get blackColor => textColor; - static Color get bgScaffoldColor => scaffoldBgColor; - - static const AppColorsDark dark = AppColorsDark(); -} - -class AppColorsDark { - const AppColorsDark(); - Color get scaffoldBgColor => const Color(0xFF191919); - Color get whiteColor => const Color(0xFF1E1E1E); - Color get primaryRedColor => const Color(0xFFDE5C5D); - Color get secondaryLightRedColor => const Color(0xFF3A1015); - Color get textColor => const Color(0xFFFFFFFF); - Color get iconColor => const Color(0xFFFFFFFF); - Color get greyColor => const Color(0xFF2C2C2E); - Color get textColorLight => const Color(0xFFB0B0B0); - Color get dividerColor => const Color(0x33FFFFFF); - Color get borderGrayColor => const Color(0x55ECECEC); - Color get greyTextColor => const Color(0xFF8F9AA3); - Color get inputLabelTextColor => const Color(0xFF9E9E9E); - Color get errorColor => const Color(0xFFD63D48); - Color get warningColor => const Color(0xFFFFCC00); - Color get infoColor => const Color(0xFF4DA6FF); - Color get shimmerBaseColor => const Color(0xFF2C2C2E); - Color get shimmerHighlightColor => const Color(0xFF3A3A3C); -} -``` - -### 8.2 FILE: `lib/theme/app_theme.dart` - -```dart -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:{{DART_PACKAGE}}/theme/colors.dart'; - -class AppTheme { - static ThemeData getTheme(bool isArabic) => ThemeData( - fontFamily: isArabic ? '{{SECONDARY_FONT}}' : '{{PRIMARY_FONT}}', - primarySwatch: Colors.red, - brightness: Brightness.light, - visualDensity: VisualDensity.adaptivePlatformDensity, - pageTransitionsTheme: const PageTransitionsTheme(builders: { - TargetPlatform.android: ZoomPageTransitionsBuilder(), - TargetPlatform.iOS: CupertinoPageTransitionsBuilder(), - }), - canvasColor: Colors.white, - splashColor: Colors.transparent, - appBarTheme: const AppBarTheme(elevation: 0, systemOverlayStyle: SystemUiOverlayStyle.light, surfaceTintColor: Colors.transparent), - ); - - static ThemeData getDarkTheme(bool isArabic) => ThemeData( - fontFamily: isArabic ? '{{SECONDARY_FONT}}' : '{{PRIMARY_FONT}}', - primarySwatch: Colors.red, - brightness: Brightness.dark, - visualDensity: VisualDensity.adaptivePlatformDensity, - pageTransitionsTheme: const PageTransitionsTheme(builders: { - TargetPlatform.android: ZoomPageTransitionsBuilder(), - TargetPlatform.iOS: CupertinoPageTransitionsBuilder(), - }), - canvasColor: AppColors.dark.scaffoldBgColor, - scaffoldBackgroundColor: AppColors.dark.scaffoldBgColor, - splashColor: Colors.transparent, - appBarTheme: AppBarTheme(color: AppColors.dark.scaffoldBgColor, elevation: 0, systemOverlayStyle: SystemUiOverlayStyle.light, surfaceTintColor: Colors.transparent), - ); -} -``` - ---- - -## 9. EXTENSIONS - -### 9.1 FILE: `lib/extensions/int_extensions.dart` - -```dart -import 'package:flutter/material.dart'; - -extension IntExtensions on int { - Widget get height => SizedBox(height: toDouble()); - Widget get width => SizedBox(width: toDouble()); - Widget get divider => Divider(height: toDouble(), thickness: toDouble(), color: const Color(0x30D2D2D2)); -} -``` - -### 9.2 FILE: `lib/extensions/string_extensions.dart` - -```dart -import 'package:flutter/material.dart'; -import 'package:{{DART_PACKAGE}}/core/utils/size_utils.dart'; -import 'package:{{DART_PACKAGE}}/theme/colors.dart'; - -extension CapExtension on String { - String get inCaps => isEmpty ? '' : '${this[0].toUpperCase()}${substring(1)}'; - String get capitalizeFirstOfEach => trim().isEmpty ? '' : trim().toLowerCase().split(' ').map((s) => s.inCaps).join(' '); -} - -extension StringToWidget on String { - Widget _text(double size, {Color? color, FontWeight? fontWeight, bool isBold = false, bool isCenter = false, bool isUnderLine = false, int? maxLines, TextOverflow? overflow, double letterSpacing = 0}) { - return Text(this, - textAlign: isCenter ? TextAlign.center : null, - maxLines: maxLines, - overflow: overflow, - style: TextStyle(fontSize: size.f, fontWeight: fontWeight ?? (isBold ? FontWeight.bold : FontWeight.normal), color: color ?? AppColors.blackColor, letterSpacing: letterSpacing, decoration: isUnderLine ? TextDecoration.underline : null, decorationColor: color ?? AppColors.blackColor), - ); - } - - Widget toText8({Color? color, FontWeight? fontWeight, bool isBold = false, int? maxLines, TextOverflow? overflow}) => _text(8, color: color, fontWeight: fontWeight, isBold: isBold, maxLines: maxLines, overflow: overflow); - Widget toText10({Color? color, FontWeight? fontWeight, bool isBold = false, bool isCenter = false, int? maxLines, TextOverflow? overflow}) => _text(10, color: color, fontWeight: fontWeight, isBold: isBold, isCenter: isCenter, maxLines: maxLines, overflow: overflow); - Widget toText11({Color? color, FontWeight? fontWeight, bool isBold = false, bool isCenter = false, int? maxLines}) => _text(11, color: color, fontWeight: fontWeight, isBold: isBold, isCenter: isCenter, maxLines: maxLines); - Widget toText12({Color? color, FontWeight? fontWeight, bool isBold = false, bool isCenter = false, int? maxLines, TextOverflow? overflow}) => _text(12, color: color, fontWeight: fontWeight, isBold: isBold, isCenter: isCenter, maxLines: maxLines, overflow: overflow); - Widget toText13({Color? color, FontWeight? fontWeight, bool isBold = false, bool isCenter = false, int? maxLines, TextOverflow? overflow}) => _text(13, color: color, fontWeight: fontWeight, isBold: isBold, isCenter: isCenter, maxLines: maxLines, overflow: overflow); - Widget toText14({Color? color, FontWeight? fontWeight, bool isBold = false, bool isCenter = false, int? maxLines, TextOverflow? overflow}) => _text(14, color: color, fontWeight: fontWeight, isBold: isBold, isCenter: isCenter, maxLines: maxLines, overflow: overflow); - Widget toText16({Color? color, FontWeight? fontWeight, bool isBold = false, bool isCenter = false, int? maxLines, TextOverflow? overflow}) => _text(16, color: color, fontWeight: fontWeight, isBold: isBold, isCenter: isCenter, maxLines: maxLines, overflow: overflow); - Widget toText18({Color? color, FontWeight? fontWeight, bool isBold = false, bool isCenter = false, int? maxLines}) => _text(18, color: color, fontWeight: fontWeight, isBold: isBold, isCenter: isCenter, maxLines: maxLines); - Widget toText20({Color? color, FontWeight? fontWeight, bool isBold = false, bool isCenter = false}) => _text(20, color: color, fontWeight: fontWeight, isBold: isBold, isCenter: isCenter); - Widget toText24({Color? color, FontWeight? fontWeight, bool isBold = false, bool isCenter = false}) => _text(24, color: color, fontWeight: fontWeight, isBold: isBold, isCenter: isCenter); - Widget toText30({Color? color, FontWeight? fontWeight, bool isBold = false, bool isCenter = false}) => _text(30, color: color, fontWeight: fontWeight, isBold: isBold, isCenter: isCenter); -} -``` - -### 9.3 FILE: `lib/extensions/widget_extensions.dart` - -```dart -import 'package:flutter/material.dart'; -import 'package:shimmer/shimmer.dart'; -import 'package:{{DART_PACKAGE}}/theme/colors.dart'; - -extension WidgetExtensions on Widget { - Widget onPress(VoidCallback onTap) => InkWell(onTap: onTap, child: this); - Widget get expanded => Expanded(child: this); - Widget get center => Center(child: this); - Widget circle(double v) => ClipRRect(borderRadius: BorderRadius.circular(v), child: this); - Widget paddingAll(double v) => Padding(padding: EdgeInsets.all(v), child: this); - Widget paddingSymmetrical(double h, double v) => Padding(padding: EdgeInsets.symmetric(horizontal: h, vertical: v), child: this); - Widget paddingOnly({double left = 0, double right = 0, double top = 0, double bottom = 0}) => Padding(padding: EdgeInsetsDirectional.only(start: left, end: right, top: top, bottom: bottom), child: this); - Widget toShimmer({bool isShow = true}) => isShow ? Shimmer.fromColors(baseColor: AppColors.shimmerBaseColor, highlightColor: AppColors.shimmerHighlightColor, child: this) : this; - Widget objectContainerView({double radius = 15}) => Container( - padding: const EdgeInsets.all(15), - decoration: BoxDecoration(color: AppColors.whiteColor, borderRadius: BorderRadius.circular(radius), boxShadow: [BoxShadow(color: const Color(0xff000000).withOpacity(.15), blurRadius: 26, offset: const Offset(0, -3))]), - child: this, - ); -} -``` - -### 9.4 FILE: `lib/extensions/context_extensions.dart` - -```dart -import 'package:flutter/material.dart'; - -extension ContextUtils on BuildContext { - double get screenHeight => MediaQuery.of(this).size.height; - double get screenWidth => MediaQuery.of(this).size.width; - ThemeData get theme => Theme.of(this); - TextTheme get textTheme => theme.textTheme; -} - -extension ShowBottomSheetExt on BuildContext { - Future showSheet({required Widget child, bool isScrollControlled = true, bool isDismissible = true}) { - return showModalBottomSheet(context: this, isScrollControlled: isScrollControlled, isDismissible: isDismissible, backgroundColor: Colors.transparent, builder: (_) => child); - } -} -``` - -### 9.5 FILE: `lib/extensions/route_extensions.dart` - -```dart -import 'package:flutter/material.dart'; - -extension NavigationExtensions on BuildContext { - void navigateWithName(String routeName, {Object? arguments}) => Navigator.pushNamed(this, routeName, arguments: arguments); - Future navigateReplaceWithName(String routeName, {Object? arguments}) async => await Navigator.pushReplacementNamed(this, routeName, arguments: arguments); - void navigateAndRemoveAll(String routeName) => Navigator.pushNamedAndRemoveUntil(this, routeName, (route) => false); - void pop() => Navigator.of(this).pop(); - void pushTo(Widget page) => Navigator.push(this, MaterialPageRoute(builder: (_) => page)); -} -``` - ---- - -## 10. WIDGETS — Starter Widgets - -### 10.1 FILE: `lib/widgets/buttons/custom_button.dart` - -```dart -import 'package:flutter/material.dart'; -import 'package:{{DART_PACKAGE}}/core/utils/size_utils.dart'; -import 'package:{{DART_PACKAGE}}/theme/colors.dart'; - -class CustomButton extends StatelessWidget { - final String label; - final VoidCallback? onPressed; - final Color? color; - final Color? textColor; - final double? width; - final bool isOutlined; - final bool isLoading; - - const CustomButton({super.key, required this.label, this.onPressed, this.color, this.textColor, this.width, this.isOutlined = false, this.isLoading = false}); - - @override - Widget build(BuildContext context) { - final bg = color ?? AppColors.primaryRedColor; - return SizedBox( - width: width ?? double.infinity, - height: 48.h, - child: isOutlined - ? OutlinedButton(onPressed: isLoading ? null : onPressed, style: OutlinedButton.styleFrom(side: BorderSide(color: bg), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12.r))), child: _child(bg)) - : ElevatedButton(onPressed: isLoading ? null : onPressed, style: ElevatedButton.styleFrom(backgroundColor: bg, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12.r))), child: _child(textColor ?? Colors.white)), - ); - } - - Widget _child(Color c) => isLoading ? SizedBox(height: 20, width: 20, child: CircularProgressIndicator(strokeWidth: 2, color: c)) : Text(label, style: TextStyle(fontSize: 14.f, fontWeight: FontWeight.w600, color: c)); -} -``` - -### 10.2 FILE: `lib/widgets/routes/fade_page.dart` - -```dart -import 'package:flutter/material.dart'; - -class FadePage extends PageRouteBuilder { - final Widget page; - FadePage({required this.page}) : super( - pageBuilder: (_, __, ___) => page, - transitionsBuilder: (_, a, __, child) => FadeTransition(opacity: a, child: child), - transitionDuration: const Duration(milliseconds: 300), - ); -} -``` - ---- - -## 11. DEPENDENCY INJECTION — `lib/core/dependencies.dart` - -```dart -import 'package:firebase_messaging/firebase_messaging.dart'; -import 'package:flutter_local_notifications/flutter_local_notifications.dart'; -import 'package:get_it/get_it.dart'; -import 'package:logger/logger.dart'; -import 'package:shared_preferences/shared_preferences.dart'; - -import 'package:{{DART_PACKAGE}}/core/api/api_client.dart'; -import 'package:{{DART_PACKAGE}}/core/app_state.dart'; -import 'package:{{DART_PACKAGE}}/services/analytics/analytics_service.dart'; -import 'package:{{DART_PACKAGE}}/services/cache_service.dart'; -import 'package:{{DART_PACKAGE}}/services/dialog_service.dart'; -import 'package:{{DART_PACKAGE}}/services/error_handler_service.dart'; -import 'package:{{DART_PACKAGE}}/services/firebase_service.dart'; -import 'package:{{DART_PACKAGE}}/services/logger_service.dart'; -import 'package:{{DART_PACKAGE}}/services/navigation_service.dart'; -import 'package:{{DART_PACKAGE}}/services/notification_service.dart'; - -final GetIt getIt = GetIt.instance; - -class AppDependencies { - static Future addDependencies() async { - final logger = Logger(printer: PrettyPrinter(methodCount: 2, errorMethodCount: 5, lineLength: 1000, colors: true, printEmojis: true)); - - getIt.registerLazySingleton(() => LoggerServiceImp(logger: logger)); - getIt.registerLazySingleton(() => NavigationService()); - getIt.registerLazySingleton(() => AppState(navigationService: getIt())); - getIt.registerLazySingleton(() => GAnalytics()); - getIt.registerLazySingleton(() => DialogServiceImp(navigationService: getIt())); - getIt.registerLazySingleton(() => ErrorHandlerServiceImp(dialogService: getIt(), loggerService: getIt(), navigationService: getIt())); - - final sp = await SharedPreferences.getInstance(); - getIt.registerLazySingleton(() => CacheServiceImp(sharedPreferences: sp, loggerService: getIt())); - getIt.registerLazySingleton(() => ApiClientImp(appState: getIt())); - getIt.registerLazySingleton(() => FirebaseServiceImpl(firebaseMessaging: FirebaseMessaging.instance, loggerService: getIt(), appState: getIt())); - - final flnp = FlutterLocalNotificationsPlugin(); - getIt.registerLazySingleton(() => NotificationServiceImp(flutterLocalNotificationsPlugin: flnp, loggerService: getIt())); - - // ═══ Register feature repos & ViewModels below as you add them ═══ - } -} -``` - ---- - -## 12. ROUTING — `lib/routes/app_routes.dart` - -```dart -import 'package:flutter/material.dart'; -import 'package:{{DART_PACKAGE}}/presentation/home/navigation_screen.dart'; -import 'package:{{DART_PACKAGE}}/splash_page.dart'; - -class AppRoutes { - static const String initialRoute = '/'; - static const String landingScreen = '/landingScreen'; - - // Add route names as you build features: - // static const String loginScreen = '/loginScreen'; - - static Map get routes => { - initialRoute: (_) => const SplashPage(), - landingScreen: (_) => const LandingNavigation(), - }; -} -``` - ---- - -## 13. LOCALIZATION FILES - -### FILE: `assets/langs/en-US.json` - -```json -{ - "appName": "{{APP_NAME}}", - "home": "Home", - "services": "Services", - "medicalFile": "Medical File", - "bookAppointment": "Book Appointment", - "login": "Login", - "register": "Register", - "cancel": "Cancel", - "confirm": "Confirm", - "done": "Done", - "ok": "OK", - "loading": "Loading...", - "noDataAvailable": "No data available", - "noInternetConnection": "No internet connection", - "settings": "Settings", - "language": "Language", - "darkMode": "Dark Mode", - "logout": "Logout", - "search": "Search" -} -``` - -### FILE: `assets/langs/ar-SA.json` - -```json -{ - "appName": "{{APP_NAME}}", - "home": "الرئيسية", - "services": "الخدمات", - "medicalFile": "الملف الطبي", - "bookAppointment": "حجز موعد", - "login": "تسجيل الدخول", - "register": "التسجيل", - "cancel": "إلغاء", - "confirm": "تأكيد", - "done": "تم", - "ok": "حسنا", - "loading": "جاري التحميل...", - "noDataAvailable": "لا توجد بيانات", - "noInternetConnection": "لا يوجد اتصال بالإنترنت", - "settings": "الإعدادات", - "language": "اللغة", - "darkMode": "الوضع الداكن", - "logout": "تسجيل الخروج", - "search": "بحث" -} -``` - ---- - -## 14. PRESENTATION — Starter Screens - -### FILE: `lib/splash_page.dart` - -```dart -import 'dart:async'; -import 'package:flutter/material.dart'; -import 'package:{{DART_PACKAGE}}/core/api_consts.dart'; -import 'package:{{DART_PACKAGE}}/core/dependencies.dart'; -import 'package:{{DART_PACKAGE}}/presentation/home/navigation_screen.dart'; -import 'package:{{DART_PACKAGE}}/services/notification_service.dart'; -import 'package:{{DART_PACKAGE}}/theme/colors.dart'; -import 'package:{{DART_PACKAGE}}/widgets/routes/fade_page.dart'; - -class SplashPage extends StatefulWidget { - const SplashPage({super.key}); - @override - State createState() => _SplashPageState(); -} - -class _SplashPageState extends State { - @override - void initState() { - super.initState(); - _init(); - } - - Future _init() async { - ApiConsts.setBackendURLs(); - await getIt.get().initialize(); - Timer(const Duration(seconds: 2), () { - Navigator.of(context).pushReplacement(FadePage(page: const LandingNavigation())); - }); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: AppColors.whiteColor, - body: Center(child: Text('{{APP_NAME}}', style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold, color: AppColors.primaryRedColor))), - ); - } -} -``` - -### FILE: `lib/presentation/home/navigation_screen.dart` - -```dart -import 'package:flutter/material.dart'; -import 'package:{{DART_PACKAGE}}/presentation/home/landing_page.dart'; -import 'package:{{DART_PACKAGE}}/theme/colors.dart'; - -class LandingNavigation extends StatefulWidget { - const LandingNavigation({super.key}); - @override - State createState() => _LandingNavigationState(); -} - -class _LandingNavigationState extends State { - int _idx = 0; - final _pc = PageController(); - - final _pages = const [ - LandingPage(), - Center(child: Text('Medical File')), - Center(child: Text('Book Appointment')), - Center(child: Text('Services')), - ]; - - @override - Widget build(BuildContext context) { - return Scaffold( - body: PageView(controller: _pc, physics: const NeverScrollableScrollPhysics(), children: _pages), - bottomNavigationBar: BottomNavigationBar( - currentIndex: _idx, - type: BottomNavigationBarType.fixed, - selectedItemColor: AppColors.primaryRedColor, - unselectedItemColor: AppColors.greyTextColor, - onTap: (i) { setState(() => _idx = i); _pc.jumpToPage(i); }, - items: const [ - BottomNavigationBarItem(icon: Icon(Icons.home_outlined), activeIcon: Icon(Icons.home), label: 'Home'), - BottomNavigationBarItem(icon: Icon(Icons.folder_outlined), activeIcon: Icon(Icons.folder), label: 'File'), - BottomNavigationBarItem(icon: Icon(Icons.calendar_today_outlined), activeIcon: Icon(Icons.calendar_today), label: 'Book'), - BottomNavigationBarItem(icon: Icon(Icons.grid_view_outlined), activeIcon: Icon(Icons.grid_view), label: 'Services'), - ], - ), - ); - } -} -``` - -### FILE: `lib/presentation/home/landing_page.dart` - -```dart -import 'package:flutter/material.dart'; -import 'package:{{DART_PACKAGE}}/extensions/int_extensions.dart'; -import 'package:{{DART_PACKAGE}}/extensions/string_extensions.dart'; -import 'package:{{DART_PACKAGE}}/theme/colors.dart'; - -class LandingPage extends StatelessWidget { - const LandingPage({super.key}); - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: AppColors.scaffoldBgColor, - body: SafeArea( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - 24.height, - 'Welcome'.toText24(isBold: true), - 8.height, - 'What would you like to do today?'.toText14(color: AppColors.greyTextColor), - 24.height, - Expanded(child: Center(child: 'Home content goes here'.toText16(color: AppColors.textColorLight, isCenter: true))), - ]), - ), - ), - ); - } -} -``` - ---- - -## 15. ENTRY POINT — `lib/main.dart` - -```dart -import 'dart:io'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:provider/provider.dart'; -import 'package:sizer/sizer.dart'; -import 'package:{{DART_PACKAGE}}/core/dependencies.dart'; -import 'package:{{DART_PACKAGE}}/routes/app_routes.dart'; -import 'package:{{DART_PACKAGE}}/services/navigation_service.dart'; -import 'package:{{DART_PACKAGE}}/theme/app_theme.dart'; -import 'package:{{DART_PACKAGE}}/theme/colors.dart'; - -class MyHttpOverrides extends HttpOverrides { - @override - HttpClient createHttpClient(SecurityContext? context) => - super.createHttpClient(context)..badCertificateCallback = (cert, host, port) => true; -} - -Future _init() async { - WidgetsFlutterBinding.ensureInitialized(); - await EasyLocalization.ensureInitialized(); - // Uncomment when Firebase is configured: - // await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); - await AppDependencies.addDependencies(); - SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); - HttpOverrides.global = MyHttpOverrides(); -} - -void main() async { - await _init(); - runApp( - EasyLocalization( - supportedLocales: const [Locale('en', 'US'), Locale('ar', 'SA')], - path: 'assets/langs', - fallbackLocale: const Locale('en', 'US'), - child: MultiProvider( - providers: [ - // ═══ Add ChangeNotifierProviders as you build features ═══ - ], - child: const MyApp(), - ), - ), - ); -} - -class MyApp extends StatelessWidget { - const MyApp({super.key}); - - @override - Widget build(BuildContext context) { - return SafeArea( - top: false, - bottom: !Platform.isIOS, - child: LayoutBuilder(builder: (context, constraints) { - return Sizer(builder: (context, orientation, deviceType) { - final isArabic = EasyLocalization.of(context)?.locale.languageCode == "ar"; - return MaterialApp( - title: '{{APP_NAME}}', - debugShowCheckedModeBanner: false, - localizationsDelegates: context.localizationDelegates, - supportedLocales: context.supportedLocales, - locale: context.locale, - initialRoute: AppRoutes.initialRoute, - routes: AppRoutes.routes, - theme: AppTheme.getTheme(isArabic), - darkTheme: AppTheme.getDarkTheme(isArabic), - themeMode: AppColors.isDarkMode ? ThemeMode.dark : ThemeMode.light, - navigatorKey: getIt.get().navigatorKey, - builder: (context, child) => MediaQuery( - data: MediaQuery.of(context).copyWith(textScaler: TextScaler.noScaling, alwaysUse24HourFormat: true), - child: child!, - ), - ); - }); - }), - ); - } -} -``` - ---- - -## 16. FEATURE MODULE TEMPLATE - -When asked to **add a new feature**, create these four files. Replace `{{feature}}` (snake_case) and `{{Feature}}` (PascalCase). - -### `lib/features/{{feature}}/{{feature}}_repo.dart` - -```dart -import 'package:dartz/dartz.dart'; -import 'package:{{DART_PACKAGE}}/core/api/api_client.dart'; -import 'package:{{DART_PACKAGE}}/core/common_models/generic_api_model.dart'; -import 'package:{{DART_PACKAGE}}/core/exceptions/api_failure.dart'; -import 'package:{{DART_PACKAGE}}/services/logger_service.dart'; - -abstract class {{Feature}}Repo { - // Future>>> getItems(); -} - -class {{Feature}}RepoImp implements {{Feature}}Repo { - final ApiClient apiClient; - final LoggerService loggerService; - {{Feature}}RepoImp({required this.loggerService, required this.apiClient}); - - // @override - // Future>>> getItems() async { - // Map body = {}; - // try { - // GenericApiModel>? result; - // Failure? failure; - // await apiClient.post('endpoint', body: body, - // onFailure: (e, s, {messageStatus, failureType}) => failure = failureType, - // onSuccess: (r, s, {messageStatus, errorMessage}) { - // try { result = GenericApiModel(data: (r['Key'] as List).map((e) => YourModel.fromJson(e)).toList()); } - // catch (e) { failure = DataParsingFailure(e.toString()); } - // }); - // if (failure != null) return Left(failure!); - // return Right(result!); - // } catch (e) { return Left(UnknownFailure(e.toString())); } - // } -} -``` - -### `lib/features/{{feature}}/{{feature}}_view_model.dart` - -```dart -import 'package:flutter/material.dart'; -import 'package:{{DART_PACKAGE}}/features/{{feature}}/{{feature}}_repo.dart'; -import 'package:{{DART_PACKAGE}}/services/error_handler_service.dart'; -import 'package:{{DART_PACKAGE}}/services/navigation_service.dart'; - -class {{Feature}}ViewModel extends ChangeNotifier { - final {{Feature}}Repo _repo; - final ErrorHandlerService _errorHandler; - final NavigationService _nav; - - {{Feature}}ViewModel({required {{Feature}}Repo repo, required ErrorHandlerService errorHandler, required NavigationService nav}) - : _repo = repo, _errorHandler = errorHandler, _nav = nav; - - bool isLoading = false; - - // void init() { isLoading = true; notifyListeners(); _fetch(); } - // void _fetch() async { - // final r = await _repo.getItems(); - // r.fold((f) { isLoading = false; _errorHandler.handleError(failure: f); notifyListeners(); }, - // (d) { /* items = d.data ?? []; */ isLoading = false; notifyListeners(); }); - // } -} -``` - -### `lib/presentation/{{feature}}/{{feature}}_page.dart` - -```dart -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'package:{{DART_PACKAGE}}/features/{{feature}}/{{feature}}_view_model.dart'; -import 'package:{{DART_PACKAGE}}/theme/colors.dart'; - -class {{Feature}}Page extends StatefulWidget { - const {{Feature}}Page({super.key}); - @override - State<{{Feature}}Page> createState() => _{{Feature}}PageState(); -} - -class _{{Feature}}PageState extends State<{{Feature}}Page> { - @override - void initState() { - super.initState(); - // Provider.of<{{Feature}}ViewModel>(context, listen: false).init(); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: AppColors.scaffoldBgColor, - appBar: AppBar(title: const Text('{{Feature}}')), - body: Consumer<{{Feature}}ViewModel>( - builder: (_, vm, __) { - if (vm.isLoading) return const Center(child: CircularProgressIndicator()); - return const Center(child: Text('Content here')); - }, - ), - ); - } -} -``` - -### After creating, REGISTER: - -**`dependencies.dart`:** -```dart -getIt.registerLazySingleton<{{Feature}}Repo>(() => {{Feature}}RepoImp(loggerService: getIt(), apiClient: getIt())); -getIt.registerLazySingleton<{{Feature}}ViewModel>(() => {{Feature}}ViewModel(repo: getIt(), errorHandler: getIt(), nav: getIt())); -``` - -**`main.dart` providers list:** -```dart -ChangeNotifierProvider<{{Feature}}ViewModel>(create: (_) => getIt.get<{{Feature}}ViewModel>()), -``` - -**`app_routes.dart`:** -```dart -static const String {{feature}}Page = '/{{feature}}'; -// in routes map: {{feature}}Page: (_) => const {{Feature}}Page(), -``` - ---- - -## 17. RULES — ALWAYS FOLLOW - -1. **Every service/repo = abstract class + implementation.** Never use concrete directly. -2. **All DI in `dependencies.dart`.** Only use `getIt.get()` elsewhere. -3. **All repos return `Future>>`.** -4. **All ViewModels extend `ChangeNotifier`.** Call `notifyListeners()` after state changes. -5. **ViewModels NEVER import widgets.** Use `NavigationService` for navigation. -6. **Pages use `Consumer` or `Provider.of` to read ViewModel state.** -7. **Colors from `AppColors`** — never hardcode hex in widgets. -8. **Text via string extensions** — `'Hello'.toText16()` not `Text('Hello')`. -9. **Spacing via int extensions** — `16.height` not `SizedBox(height: 16)`. -10. **Sizes via responsive extensions** — `16.f`, `16.w`, `16.h`, `16.r`. -11. **Assets via `AppAssets`** — never hardcode paths. -12. **Cache keys in `CacheConst`** — never raw strings for SharedPreferences. -13. **Endpoints in `ApiConsts`** — never hardcode URLs. -14. **Errors through `ErrorHandlerService`** — VMs never show dialogs. -15. **User-facing strings use `.tr()`** from easy_localization. - ---- - -## 18. CHECKLIST — Verify Before Delivering - -- [ ] `flutter pub get` succeeds -- [ ] All imports use `package:{{DART_PACKAGE}}/...` -- [ ] Every file in this spec exists -- [ ] `dependencies.dart` registers all existing services/repos/VMs -- [ ] `main.dart` providers list includes all registered VMs -- [ ] `app_routes.dart` includes all existing pages -- [ ] `pubspec.yaml` declares all existing asset directories -- [ ] Both `en-US.json` and `ar-SA.json` exist -- [ ] No circular imports -- [ ] `AppColors` used everywhere (no raw `Color()` in widgets) -- [ ] Extensions used for text, spacing, widget wrapping - ---- - -*End of specification. Generate all files above. When asked to add a feature, use Section 16.* - diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 1d0e8453..e852236b 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -7,6 +7,7 @@ plugins { id("com.google.gms.google-services") version "4.4.1" // Add the version here id("dev.flutter.flutter-gradle-plugin") id("com.huawei.agconnect") + id("kotlin-parcelize") // id("com.mapbox.gradle.application") // id("com.mapbox.gradle.plugins.ndk") } @@ -33,7 +34,8 @@ android { defaultConfig { applicationId = "com.cloudsolutions.HMGPatientApp" // minSdk = 24 - minSdk = 26 +// minSdk = 26 + minSdk = 29 targetSdk = 35 compileSdk = 36 // targetSdk = flutter.targetSdkVersion @@ -191,6 +193,9 @@ dependencies { implementation(files("libs/PenNavUI.aar")) implementation(files("libs/Penguin.aar")) implementation(files("libs/PenguinRenderer.aar")) + api(files("libs/samsung-health-data-api.aar")) + implementation("com.huawei.hms:health:6.11.0.300") + implementation("com.huawei.hms:hmscoreinstaller:6.6.0.300") implementation("com.github.kittinunf.fuel:fuel:2.3.1") implementation("com.github.kittinunf.fuel:fuel-android:2.3.1") diff --git a/android/app/libs/samsung-health-data-api.aar b/android/app/libs/samsung-health-data-api.aar new file mode 100644 index 00000000..1fd24034 Binary files /dev/null and b/android/app/libs/samsung-health-data-api.aar differ diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 0e77b6b6..cd5030d2 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -92,7 +92,7 @@ - + @@ -124,6 +124,7 @@ , + grantResults: IntArray + ) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + + val granted = grantResults.all { it == PackageManager.PERMISSION_GRANTED } + val intent = Intent("PERMISSION_RESULT_ACTION").apply { + putExtra("PERMISSION_GRANTED", granted) + } + sendBroadcast(intent) + + // Log the request code and permission results + Log.d("PermissionsResult", "Request Code: $requestCode") + Log.d("PermissionsResult", "Permissions: ${permissions.joinToString()}") + Log.d("PermissionsResult", "Grant Results: ${grantResults.joinToString()}") + + } + + override fun onResume() { + super.onResume() + } + +// override fun onActivityResult(requestCode: Int, resultCode: Int, @Nullable data: Intent?) { +// super.onActivityResult(requestCode, resultCode, data) +// +// // Process only the response result of the authorization process. +// if (requestCode == 1002) { +// // Obtain the authorization response result from the intent. +// val result: HealthKitAuthResult? = huaweiWatch?.mSettingController?.parseHealthKitAuthResultFromIntent(data) +// if (result == null) { +// Log.w(huaweiWatch?.TAG, "authorization fail") +// return +// } +// +// if (result.isSuccess) { +// Log.i(huaweiWatch?.TAG, "authorization success") +// if (result.getAuthAccount() != null && result.authAccount.authorizedScopes != null) { +// val authorizedScopes: MutableSet = result.authAccount.authorizedScopes +// if(authorizedScopes.isNotEmpty()) { +// huaweiWatch?.getHealthAppAuthorization() +// } +// } +// } else { +// Log.w("MainActivty", "authorization fail, errorCode:" + result.getErrorCode()) +// } +// } +// } +} diff --git a/android/app/src/main/kotlin/com/cloudsolutions/HMGPatientApp/PenguinInPlatformBridge.kt b/android/app/src/main/kotlin/com/cloudsolutions/HMGPatientApp/PenguinInPlatformBridge.kt new file mode 100644 index 00000000..5fae68ca --- /dev/null +++ b/android/app/src/main/kotlin/com/cloudsolutions/HMGPatientApp/PenguinInPlatformBridge.kt @@ -0,0 +1,60 @@ +package com.cloudsolutions.HMGPatientApp + +import android.os.Build +import android.util.Log +import androidx.annotation.RequiresApi +import com.cloudsolutions.HMGPatientApp.penguin.PenguinView +import io.flutter.embedding.engine.FlutterEngine +import io.flutter.plugin.common.MethodCall +import com.cloudsolutions.HMGPatientApp.PermissionManager.HostNotificationPermissionManager +import com.cloudsolutions.HMGPatientApp.PermissionManager.HostBgLocationManager +import com.cloudsolutions.HMGPatientApp.PermissionManager.HostGpsStateManager +import io.flutter.plugin.common.MethodChannel + +class PenguinInPlatformBridge( + private var flutterEngine: FlutterEngine, + private var mainActivity: MainActivity +) { + + private lateinit var channel: MethodChannel + + companion object { + private const val CHANNEL = "launch_penguin_ui" + } + + @RequiresApi(Build.VERSION_CODES.O) + fun create() { +// openTok = OpenTok(mainActivity, flutterEngine) + channel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL) + channel.setMethodCallHandler { call: MethodCall, result: MethodChannel.Result -> + when (call.method) { + "launchPenguin" -> { + print("the platform channel is being called") + + if (HostNotificationPermissionManager.isNotificationPermissionGranted(mainActivity)) + else HostNotificationPermissionManager.requestNotificationPermission(mainActivity) + HostBgLocationManager.requestLocationBackgroundPermission(mainActivity) + HostGpsStateManager.requestLocationPermission(mainActivity) + val args = call.arguments as Map? + Log.d("TAG", "configureFlutterEngine: $args") + println("args") + args?.let { + PenguinView( + mainActivity, + 100, + args, + flutterEngine.dartExecutor.binaryMessenger, + activity = mainActivity, + channel + ) + } + } + + else -> { + result.notImplemented() + } + } + } + } + +} diff --git a/android/app/src/main/kotlin/com/cloudsolutions/HMGPatientApp/PermissionManager/AppPreferences.java b/android/app/src/main/kotlin/com/cloudsolutions/HMGPatientApp/PermissionManager/AppPreferences.java new file mode 100644 index 00000000..2f6c9722 --- /dev/null +++ b/android/app/src/main/kotlin/com/cloudsolutions/HMGPatientApp/PermissionManager/AppPreferences.java @@ -0,0 +1,139 @@ +package com.cloudsolutions.HMGPatientApp.PermissionManager; + +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Handler; +import android.os.HandlerThread; + +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.FutureTask; + + +/** + * This preferences for app level + */ + +public class AppPreferences { + + public static final String PREF_NAME = "PenguinINUI_AppPreferences"; + public static final int MODE = Context.MODE_PRIVATE; + + public static final String campusIdKey = "campusId"; + + public static final String LANG = "Lang"; + + public static final String settingINFO = "SETTING-INFO"; + + public static final String userName = "userName"; + public static final String passWord = "passWord"; + + private static HandlerThread handlerThread; + private static Handler handler; + + static { + handlerThread = new HandlerThread("PreferencesHandlerThread"); + handlerThread.start(); + handler = new Handler(handlerThread.getLooper()); + } + + + + public static SharedPreferences getPreferences(final Context context) { + return context.getSharedPreferences(AppPreferences.PREF_NAME, AppPreferences.MODE); + } + + public static SharedPreferences.Editor getEditor(final Context context) { + return getPreferences(context).edit(); + } + + + public static void writeInt(final Context context, final String key, final int value) { + handler.post(() -> { + SharedPreferences.Editor editor = getEditor(context); + editor.putInt(key, value); + editor.apply(); + }); + } + + + public static int readInt(final Context context, final String key, final int defValue) { + Callable callable = () -> { + SharedPreferences preferences = getPreferences(context); + return preferences.getInt(key, -1); + }; + + Future future = new FutureTask<>(callable); + handler.post((Runnable) future); + + try { + return future.get(); + } catch (InterruptedException | ExecutionException e) { + e.printStackTrace(); // Handle the exception appropriately + } + + return -1; // Return the default value in case of an error + } + + public static int getCampusId(final Context context) { + return readInt(context,campusIdKey,-1); + } + + + + public static void writeString(final Context context, final String key, final String value) { + handler.post(() -> { + SharedPreferences.Editor editor = getEditor(context); + editor.putString(key, value); + editor.apply(); + }); + } + + + public static String readString(final Context context, final String key, final String defValue) { + Callable callable = () -> { + SharedPreferences preferences = getPreferences(context); + return preferences.getString(key, defValue); + }; + + Future future = new FutureTask<>(callable); + handler.post((Runnable) future); + + try { + return future.get(); + } catch (InterruptedException | ExecutionException e) { + e.printStackTrace(); // Handle the exception appropriately + } + + return defValue; // Return the default value in case of an error + } + + + public static void writeBoolean(final Context context, final String key, final boolean value) { + handler.post(() -> { + SharedPreferences.Editor editor = getEditor(context); + editor.putBoolean(key, value); + editor.apply(); + }); + } + + public static boolean readBoolean(final Context context, final String key, final boolean defValue) { + Callable callable = () -> { + SharedPreferences preferences = getPreferences(context); + return preferences.getBoolean(key, defValue); + }; + + Future future = new FutureTask<>(callable); + handler.post((Runnable) future); + + try { + return future.get(); + } catch (InterruptedException | ExecutionException e) { + e.printStackTrace(); // Handle the exception appropriately + } + + return defValue; // Return the default value in case of an error + } + +} diff --git a/android/app/src/main/kotlin/com/cloudsolutions/HMGPatientApp/PermissionManager/HostBgLocationManager.java b/android/app/src/main/kotlin/com/cloudsolutions/HMGPatientApp/PermissionManager/HostBgLocationManager.java new file mode 100644 index 00000000..da0d8138 --- /dev/null +++ b/android/app/src/main/kotlin/com/cloudsolutions/HMGPatientApp/PermissionManager/HostBgLocationManager.java @@ -0,0 +1,136 @@ +package com.cloudsolutions.HMGPatientApp.PermissionManager; + +import android.Manifest; +import android.app.Activity; +import android.app.AlertDialog; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.provider.Settings; + +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; + +import com.peng.pennavmap.PlugAndPlaySDK; +import com.peng.pennavmap.R; +import com.peng.pennavmap.enums.InitializationErrorType; + +/** + * Manages background location permission requests and handling for the application. + */ +public class HostBgLocationManager { + /** + * Request code for background location permission + */ + public static final int REQUEST_ACCESS_BACKGROUND_LOCATION_CODE = 301; + + /** + * Request code for navigating to app settings + */ + private static final int REQUEST_CODE_SETTINGS = 11234; + + /** + * Alert dialog for denied permissions + */ + private static AlertDialog deniedAlertDialog; + + /** + * Checks if the background location permission has been granted. + * + * @param context the context of the application or activity + * @return true if the permission is granted, false otherwise + */ + + public static boolean isLocationBackgroundGranted(Context context) { + return ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_BACKGROUND_LOCATION) + == PackageManager.PERMISSION_GRANTED; + } + + /** + * Requests the background location permission from the user. + * + * @param activity the activity from which the request is made + */ + public static void requestLocationBackgroundPermission(Activity activity) { + // Check if the ACCESS_BACKGROUND_LOCATION permission is already granted + if (!isLocationBackgroundGranted(activity)) { + // Permission is not granted, so request it + ActivityCompat.requestPermissions(activity, + new String[]{Manifest.permission.ACCESS_BACKGROUND_LOCATION}, + REQUEST_ACCESS_BACKGROUND_LOCATION_CODE); + } + } + + /** + * Displays a dialog prompting the user to grant the background location permission. + * + * @param activity the activity where the dialog is displayed + */ + public static void showLocationBackgroundPermission(Activity activity) { + AlertDialog alertDialog = new AlertDialog.Builder(activity) + .setCancelable(false) + .setMessage(activity.getString(R.string.com_penguin_nav_ui_geofence_alert_msg)) + .setPositiveButton(activity.getString(R.string.com_penguin_nav_ui_go_to_settings), (dialog, which) -> { + if (activity.shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_BACKGROUND_LOCATION)) { + HostBgLocationManager.requestLocationBackgroundPermission(activity); + } else { + openAppSettings(activity); + } + if (dialog != null) { + dialog.dismiss(); + } + }) + .setNegativeButton(activity.getString(R.string.com_penguin_nav_ui_later), (dialog, which) -> { + dialog.cancel(); + }) + .create(); + + alertDialog.show(); + } + + /** + * Handles the scenario where permissions are denied by the user. + * Displays a dialog to guide the user to app settings or exit the activity. + * + * @param activity the activity where the dialog is displayed + */ + public static synchronized void handlePermissionsDenied(Activity activity) { + if (deniedAlertDialog != null && deniedAlertDialog.isShowing()) { + deniedAlertDialog.dismiss(); + } + + AlertDialog.Builder builder = new AlertDialog.Builder(activity); + builder.setCancelable(false) + .setMessage(activity.getString(R.string.com_penguin_nav_ui_permission_denied_dialog_msg)) + .setNegativeButton(activity.getString(R.string.com_penguin_nav_ui_cancel), (dialogInterface, i) -> { + if (PlugAndPlaySDK.externalPenNavUIDelegate != null) { + PlugAndPlaySDK.externalPenNavUIDelegate.onPenNavInitializationError( + InitializationErrorType.permissions.getTypeKey(), + InitializationErrorType.permissions); + } + activity.finish(); + }) + .setPositiveButton(activity.getString(R.string.com_penguin_nav_ui_go_settings), (dialogInterface, i) -> { + dialogInterface.dismiss(); + openAppSettings(activity); + }); + deniedAlertDialog = builder.create(); + deniedAlertDialog.show(); + } + + /** + * Opens the application's settings screen to allow the user to modify permissions. + * + * @param activity the activity from which the settings screen is launched + */ + private static void openAppSettings(Activity activity) { + Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + Uri uri = Uri.fromParts("package", activity.getPackageName(), null); + intent.setData(uri); + + if (intent.resolveActivity(activity.getPackageManager()) != null) { + activity.startActivityForResult(intent, REQUEST_CODE_SETTINGS); + } + } +} diff --git a/android/app/src/main/kotlin/com/cloudsolutions/HMGPatientApp/PermissionManager/HostGpsStateManager.java b/android/app/src/main/kotlin/com/cloudsolutions/HMGPatientApp/PermissionManager/HostGpsStateManager.java new file mode 100644 index 00000000..f7f39c97 --- /dev/null +++ b/android/app/src/main/kotlin/com/cloudsolutions/HMGPatientApp/PermissionManager/HostGpsStateManager.java @@ -0,0 +1,68 @@ +package com.cloudsolutions.HMGPatientApp.PermissionManager; + +import android.Manifest; +import android.app.Activity; +import android.content.Context; +import android.content.pm.PackageManager; +import android.location.LocationManager; + +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; + +import com.peng.pennavmap.managers.permissions.managers.BgLocationManager; + +public class HostGpsStateManager { + private static final int LOCATION_PERMISSION_REQUEST_CODE = 1; + + + public boolean checkGPSEnabled(Activity activity) { + LocationManager gpsStateManager = (LocationManager) activity.getSystemService(Context.LOCATION_SERVICE); + return gpsStateManager.isProviderEnabled(LocationManager.GPS_PROVIDER); + } + + public static boolean isGpsGranted(Activity activity) { + return BgLocationManager.isLocationBackgroundGranted(activity) + || ContextCompat.checkSelfPermission( + activity, + Manifest.permission.ACCESS_FINE_LOCATION + ) == PackageManager.PERMISSION_GRANTED + && ContextCompat.checkSelfPermission( + activity, + Manifest.permission.ACCESS_COARSE_LOCATION + ) == PackageManager.PERMISSION_GRANTED; + } + + + /** + * Checks if the location permission is granted. + * + * @param activity the Activity context + * @return true if permission is granted, false otherwise + */ + public static boolean isLocationPermissionGranted(Activity activity) { + return ContextCompat.checkSelfPermission( + activity, + Manifest.permission.ACCESS_FINE_LOCATION + ) == PackageManager.PERMISSION_GRANTED && + ContextCompat.checkSelfPermission( + activity, + Manifest.permission.ACCESS_COARSE_LOCATION + ) == PackageManager.PERMISSION_GRANTED; + } + + /** + * Requests the location permission. + * + * @param activity the Activity context + */ + public static void requestLocationPermission(Activity activity) { + ActivityCompat.requestPermissions( + activity, + new String[]{ + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION, + }, + LOCATION_PERMISSION_REQUEST_CODE + ); + } +} diff --git a/android/app/src/main/kotlin/com/cloudsolutions/HMGPatientApp/PermissionManager/HostNotificationPermissionManager.java b/android/app/src/main/kotlin/com/cloudsolutions/HMGPatientApp/PermissionManager/HostNotificationPermissionManager.java new file mode 100644 index 00000000..2dac16ca --- /dev/null +++ b/android/app/src/main/kotlin/com/cloudsolutions/HMGPatientApp/PermissionManager/HostNotificationPermissionManager.java @@ -0,0 +1,73 @@ +package com.cloudsolutions.HMGPatientApp.PermissionManager; + +import android.app.Activity; +import android.content.pm.PackageManager; +import android.os.Build; + +import androidx.annotation.NonNull; +import androidx.core.app.ActivityCompat; +import androidx.core.app.NotificationManagerCompat; + +public class HostNotificationPermissionManager { + private static final int REQUEST_NOTIFICATION_PERMISSION = 100; + + + /** + * Checks if the notification permission is granted. + * + * @return true if the notification permission is granted, false otherwise. + */ + public static boolean isNotificationPermissionGranted(Activity activity) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + try { + return ActivityCompat.checkSelfPermission(activity, android.Manifest.permission.POST_NOTIFICATIONS) + == PackageManager.PERMISSION_GRANTED; + } catch (Exception e) { + // Handle cases where the API is unavailable + e.printStackTrace(); + return NotificationManagerCompat.from(activity).areNotificationsEnabled(); + } + } else { + // Permissions were not required below Android 13 for notifications + return NotificationManagerCompat.from(activity).areNotificationsEnabled(); + } + } + + /** + * Requests the notification permission. + */ + public static void requestNotificationPermission(Activity activity) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + if (!isNotificationPermissionGranted(activity)) { + ActivityCompat.requestPermissions(activity, + new String[]{android.Manifest.permission.POST_NOTIFICATIONS}, + REQUEST_NOTIFICATION_PERMISSION); + } + } + } + + /** + * Handles the result of the permission request. + * + * @param requestCode The request code passed in requestPermissions(). + * @param permissions The requested permissions. + * @param grantResults The grant results for the corresponding permissions. + */ + public static boolean handlePermissionResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + if (permissions.length > 0 && + permissions[0].equals(android.Manifest.permission.POST_NOTIFICATIONS) && + grantResults.length > 0 && + grantResults[0] == PackageManager.PERMISSION_GRANTED) { + // Permission granted + System.out.println("Notification permission granted."); + return true; + } else { + // Permission denied + System.out.println("Notification permission denied."); + return false; + } + + } + + +} diff --git a/android/app/src/main/kotlin/com/cloudsolutions/HMGPatientApp/PermissionManager/PermissionHelper.kt b/android/app/src/main/kotlin/com/cloudsolutions/HMGPatientApp/PermissionManager/PermissionHelper.kt new file mode 100644 index 00000000..9a033f36 --- /dev/null +++ b/android/app/src/main/kotlin/com/cloudsolutions/HMGPatientApp/PermissionManager/PermissionHelper.kt @@ -0,0 +1,27 @@ +package com.cloudsolutions.HMGPatientApp.PermissionManager + +import android.Manifest + +object PermissionHelper { + + fun getRequiredPermissions(): Array { + val permissions = mutableListOf( + Manifest.permission.INTERNET, + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION, + Manifest.permission.ACCESS_NETWORK_STATE, + Manifest.permission.BLUETOOTH, + Manifest.permission.BLUETOOTH_ADMIN, +// Manifest.permission.ACTIVITY_RECOGNITION + ) + + // For Android 12 (API level 31) and above, add specific permissions +// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { // Android 12 (API 31) and above + permissions.add(Manifest.permission.BLUETOOTH_SCAN) + permissions.add(Manifest.permission.BLUETOOTH_CONNECT) + permissions.add(Manifest.permission.HIGH_SAMPLING_RATE_SENSORS) +// } + + return permissions.toTypedArray() + } +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/cloudsolutions/HMGPatientApp/PermissionManager/PermissionManager.kt b/android/app/src/main/kotlin/com/cloudsolutions/HMGPatientApp/PermissionManager/PermissionManager.kt new file mode 100644 index 00000000..6dadddb1 --- /dev/null +++ b/android/app/src/main/kotlin/com/cloudsolutions/HMGPatientApp/PermissionManager/PermissionManager.kt @@ -0,0 +1,50 @@ +package com.cloudsolutions.HMGPatientApp.PermissionManager + +import android.app.Activity +import android.content.Context +import android.content.pm.PackageManager +import android.os.Build +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat + +class PermissionManager( + private val context: Context, + val listener: PermissionListener, + private val requestCode: Int, + vararg permissions: String +) { + + private val permissionsArray = permissions + + interface PermissionListener { + fun onPermissionGranted() + fun onPermissionDenied() + } + + fun arePermissionsGranted(): Boolean { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + permissionsArray.all { + ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED + } + } else { + true + } + } + + fun requestPermissions(activity: Activity) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + ActivityCompat.requestPermissions(activity, permissionsArray, requestCode) + } + } + + fun handlePermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { + if (this.requestCode == requestCode) { + val allGranted = grantResults.all { it == PackageManager.PERMISSION_GRANTED } + if (allGranted) { + listener.onPermissionGranted() + } else { + listener.onPermissionDenied() + } + } + } +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/cloudsolutions/HMGPatientApp/PermissionManager/PermissionResultReceiver.kt b/android/app/src/main/kotlin/com/cloudsolutions/HMGPatientApp/PermissionManager/PermissionResultReceiver.kt new file mode 100644 index 00000000..7c2df4cb --- /dev/null +++ b/android/app/src/main/kotlin/com/cloudsolutions/HMGPatientApp/PermissionManager/PermissionResultReceiver.kt @@ -0,0 +1,15 @@ +package com.cloudsolutions.HMGPatientApp.PermissionManager + +// PermissionResultReceiver.kt +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent + +class PermissionResultReceiver( + private val callback: (Boolean) -> Unit +) : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + val granted = intent?.getBooleanExtra("PERMISSION_GRANTED", false) ?: false + callback(granted) + } +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/cloudsolutions/HMGPatientApp/penguin/PenguinMethod.kt b/android/app/src/main/kotlin/com/cloudsolutions/HMGPatientApp/penguin/PenguinMethod.kt new file mode 100644 index 00000000..4807bcc7 --- /dev/null +++ b/android/app/src/main/kotlin/com/cloudsolutions/HMGPatientApp/penguin/PenguinMethod.kt @@ -0,0 +1,13 @@ +package com.cloudsolutions.HMGPatientApp.penguin + +enum class PenguinMethod { + // initializePenguin("initializePenguin"), + // configurePenguin("configurePenguin"), + // showPenguinUI("showPenguinUI"), + // onPenNavUIDismiss("onPenNavUIDismiss"), + // onReportIssue("onReportIssue"), + // onPenNavSuccess("onPenNavSuccess"), + onPenNavInitializationError // onLocationOffCampus("onLocationOffCampus"), + // navigateToPOI("navigateToPOI"), + // openSharedLocation("openSharedLocation"); +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/cloudsolutions/HMGPatientApp/penguin/PenguinNavigator.kt b/android/app/src/main/kotlin/com/cloudsolutions/HMGPatientApp/penguin/PenguinNavigator.kt new file mode 100644 index 00000000..29cc82df --- /dev/null +++ b/android/app/src/main/kotlin/com/cloudsolutions/HMGPatientApp/penguin/PenguinNavigator.kt @@ -0,0 +1,97 @@ +package com.cloudsolutions.HMGPatientApp.penguin + +import android.content.Context +import com.google.gson.Gson +import com.peng.pennavmap.PlugAndPlaySDK +import com.peng.pennavmap.connections.ApiController +import com.peng.pennavmap.interfaces.RefIdDelegate +import com.peng.pennavmap.models.TokenModel +import com.peng.pennavmap.models.postmodels.PostToken +import com.peng.pennavmap.utils.AppSharedData +import okhttp3.ResponseBody +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response +import android.util.Log + + +class PenguinNavigator() { + + fun navigateTo(mContext: Context, refID: String, delegate: RefIdDelegate,clientID : String,clientKey : String ) { + val postToken = PostToken(clientID, clientKey) + getToken(mContext, postToken, object : RefIdDelegate { + override fun onRefByIDSuccess(PoiId: String?) { + Log.e("navigateTo", "PoiId is+++++++ $refID") + + PlugAndPlaySDK.navigateTo(mContext, refID, object : RefIdDelegate { + override fun onRefByIDSuccess(PoiId: String?) { + Log.e("navigateTo", "PoiId 2is+++++++ $PoiId") + + delegate.onRefByIDSuccess(refID) + + } + + override fun onGetByRefIDError(error: String?) { + delegate.onRefByIDSuccess(error) + } + + }) + + + } + + override fun onGetByRefIDError(error: String?) { + delegate.onRefByIDSuccess(error) + } + + }) + + } + + fun getToken(mContext: Context, postToken: PostToken?, apiTokenCallBack: RefIdDelegate) { + try { + // Create the API call + val purposesCall: Call = ApiController.getInstance(mContext) + .apiMethods + .getToken(postToken) + + // Enqueue the call for asynchronous execution + purposesCall.enqueue(object : Callback { + override fun onResponse( + call: Call, + response: Response + ) { + if (response.isSuccessful() && response.body() != null) { + try { + response.body()?.use { responseBody -> + val responseBodyString: String = responseBody.string() // Use `string()` to get the actual response content + if (responseBodyString.isNotEmpty()) { + val tokenModel = Gson().fromJson(responseBodyString, TokenModel::class.java) + if (tokenModel != null && tokenModel.token != null) { + AppSharedData.apiToken = tokenModel.token + apiTokenCallBack.onRefByIDSuccess(tokenModel.token) + } else { + apiTokenCallBack.onGetByRefIDError("Failed to parse token model") + } + } else { + apiTokenCallBack.onGetByRefIDError("Response body is empty") + } + } + } catch (e: Exception) { + apiTokenCallBack.onGetByRefIDError("An error occurred: ${e.message}") + } + } else { + apiTokenCallBack.onGetByRefIDError("Unsuccessful response: " + response.code()) + } + } + + override fun onFailure(call: Call, t: Throwable) { + apiTokenCallBack.onGetByRefIDError(t.message) + } + }) + } catch (error: Exception) { + apiTokenCallBack.onGetByRefIDError("Exception during API call: $error") + } + } + +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/cloudsolutions/HMGPatientApp/penguin/PenguinView.kt b/android/app/src/main/kotlin/com/cloudsolutions/HMGPatientApp/penguin/PenguinView.kt new file mode 100644 index 00000000..2122e01c --- /dev/null +++ b/android/app/src/main/kotlin/com/cloudsolutions/HMGPatientApp/penguin/PenguinView.kt @@ -0,0 +1,376 @@ +package com.cloudsolutions.HMGPatientApp.penguin + +import android.app.Activity +import android.content.Context +import android.content.Context.RECEIVER_EXPORTED +import android.content.IntentFilter +import android.graphics.Color +import android.os.Build +import android.util.Log +import android.view.View +import android.view.ViewGroup +import android.widget.RelativeLayout +import android.widget.Toast +import androidx.annotation.RequiresApi +import com.cloudsolutions.HMGPatientApp.PermissionManager.PermissionManager +import com.cloudsolutions.HMGPatientApp.PermissionManager.PermissionResultReceiver +import com.cloudsolutions.HMGPatientApp.MainActivity +import com.cloudsolutions.HMGPatientApp.PermissionManager.PermissionHelper +import com.peng.pennavmap.PlugAndPlayConfiguration +import com.peng.pennavmap.PlugAndPlaySDK +import com.peng.pennavmap.enums.InitializationErrorType +import com.peng.pennavmap.interfaces.PenNavUIDelegate +import com.peng.pennavmap.utils.Languages +import io.flutter.plugin.common.BinaryMessenger +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.platform.PlatformView +import com.peng.pennavmap.interfaces.PIEventsDelegate +import com.peng.pennavmap.interfaces.PILocationDelegate +import com.peng.pennavmap.interfaces.RefIdDelegate +import com.peng.pennavmap.models.LocationMessage +import com.peng.pennavmap.models.PIReportIssue +import java.util.ArrayList +import penguin.com.pennav.renderer.PIRendererSettings + +/** + * Custom PlatformView for displaying Penguin UI components within a Flutter app. + * Implements `PlatformView` for rendering the view, `MethodChannel.MethodCallHandler` for handling method calls, + * and `PenNavUIDelegate` for handling SDK events. + */ +@RequiresApi(Build.VERSION_CODES.O) +internal class PenguinView( + context: Context, + id: Int, + val creationParams: Map, + messenger: BinaryMessenger, + activity: MainActivity, + val channel: MethodChannel +) : PlatformView, MethodChannel.MethodCallHandler, PenNavUIDelegate, PIEventsDelegate, + PILocationDelegate { + // The layout for displaying the Penguin UI + private val mapLayout: RelativeLayout = RelativeLayout(context) + private val _context: Context = context + + private val permissionResultReceiver: PermissionResultReceiver + private val permissionIntentFilter = IntentFilter("PERMISSION_RESULT_ACTION") + + private companion object { + const val PERMISSIONS_REQUEST_CODE = 1 + } + + private lateinit var permissionManager: PermissionManager + + // Reference to the main activity + private var _activity: Activity = activity + + private lateinit var mContext: Context + + lateinit var navigator: PenguinNavigator + + init { + // Set layout parameters for the mapLayout + mapLayout.layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT + ) + + mContext = context + + + permissionResultReceiver = PermissionResultReceiver { granted -> + if (granted) { + onPermissionsGranted() + } else { + onPermissionsDenied() + } + } + if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + mContext.registerReceiver( + permissionResultReceiver, + permissionIntentFilter, + RECEIVER_EXPORTED + ) + } else { + mContext.registerReceiver( + permissionResultReceiver, + permissionIntentFilter, + ) + } + + // Set the background color of the layout + mapLayout.setBackgroundColor(Color.RED) + + permissionManager = PermissionManager( + context = mContext, + listener = object : PermissionManager.PermissionListener { + override fun onPermissionGranted() { + // Handle permissions granted + onPermissionsGranted() + } + + override fun onPermissionDenied() { + // Handle permissions denied + onPermissionsDenied() + } + }, + requestCode = PERMISSIONS_REQUEST_CODE, + PermissionHelper.getRequiredPermissions().get(0) + ) + + if (!permissionManager.arePermissionsGranted()) { + permissionManager.requestPermissions(_activity) + } else { + // Permissions already granted + permissionManager.listener.onPermissionGranted() + } + + + } + + private fun onPermissionsGranted() { + // Handle the actions when permissions are granted + Log.d("PermissionsResult", "onPermissionsGranted") + // Register the platform view factory for creating custom views + + // Initialize the Penguin SDK + initPenguin() + + + } + + private fun onPermissionsDenied() { + // Handle the actions when permissions are denied + Log.d("PermissionsResult", "onPermissionsDenied") + + } + + /** + * Returns the view associated with this PlatformView. + * + * @return The main view for this PlatformView. + */ + override fun getView(): View { + return mapLayout + } + + /** + * Cleans up resources associated with this PlatformView. + */ + override fun dispose() { + // Cleanup code if needed + } + + /** + * Handles method calls from Dart code. + * + * @param call The method call from Dart. + * @param result The result callback to send responses back to Dart. + */ + override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { + // Handle method calls from Dart code here + } + + /** + * Initializes the Penguin SDK with custom configuration and delegates. + */ + private fun initPenguin() { + navigator = PenguinNavigator() + // Configure the PlugAndPlaySDK + val language = when (creationParams["languageCode"] as String) { + "ar" -> Languages.ar + "en" -> Languages.en + else -> { + Languages.en + } + } + + +// PlugAndPlaySDK.configuration = Builder() +// .setClientData(MConstantsDemo.CLIENT_ID, MConstantsDemo.CLIENT_KEY) +// .setLanguageID(selectedLanguage) +// .setBaseUrl(MConstantsDemo.DATA_URL, MConstantsDemo.POSITION_URL) +// .setServiceName(MConstantsDemo.DATA_SERVICE_NAME, MConstantsDemo.POSITION_SERVICE_NAME) +// .setUserName(name) +// .setSimulationModeEnabled(isSimulation) +// .setCustomizeColor(if (MConstantsDemo.APP_COLOR != null) MConstantsDemo.APP_COLOR else "#2CA0AF") +// .setEnableBackButton(MConstantsDemo.SHOW_BACK_BUTTON) +// .setCampusId(MConstantsDemo.selectedCampusId) +// +// .setShowUILoader(true) +// .build() + + PIRendererSettings.styleUri = "mapbox://styles/rwaid/cm3h30b36007v01qz7ik8a0sk" + + PlugAndPlaySDK.configuration = PlugAndPlayConfiguration.Builder() + .setBaseUrl( + creationParams["dataURL"] as String, + creationParams["positionURL"] as String + ) + .setServiceName( + creationParams["dataServiceName"] as String, + creationParams["positionServiceName"] as String + ) + .setClientData( + creationParams["clientID"] as String, + creationParams["clientKey"] as String + ) + .setUserName(creationParams["username"] as String) +// .setLanguageID(Languages.en) + .setLanguageID(language) + .setSimulationModeEnabled(creationParams["isSimulationModeEnabled"] as Boolean) + .setEnableBackButton(true) +// .setDeepLinkData("deeplink") + .setCustomizeColor("#2CA0AF") + .setDeepLinkSchema("", "") + .setIsEnableReportIssue(true) + .setDeepLinkData("") + .setEnableSharedLocationCallBack(false) + .setShowUILoader(true) + .setCampusId(creationParams["projectID"] as Int) + .build() + + + Log.d( + "TAG", + "initPenguin: ${creationParams["projectID"]}" + ) + + Log.d( + "TAG", + "initPenguin: creation param are ${creationParams}" + ) + + // Set location delegate to handle location updates +// PlugAndPlaySDK.setPiLocationDelegate { + // Example code to handle location updates + // Uncomment and modify as needed + // if (location.size() > 0) + // Toast.makeText(_context, "Location Info Latitude: ${location[0]}, Longitude: ${location[1]}", Toast.LENGTH_SHORT).show() +// } + + // Set events delegate for reporting issues +// PlugAndPlaySDK.setPiEventsDelegate(new PIEventsDelegate() { +// @Override +// public void onReportIssue(PIReportIssue issue) { +// Log.e("Issue Reported: ", issue.getReportType()); +// } +// // Implement issue reporting logic here } +// @Override +// public void onSharedLocation(String link) { +// // Implement Shared location logic here +// } +// }) + + // Start the Penguin SDK + PlugAndPlaySDK.setPiEventsDelegate(this) + PlugAndPlaySDK.setPiLocationDelegate(this) + PlugAndPlaySDK.start(mContext, this) + } + + + /** + * Navigates to the specified reference ID. + * + * @param refID The reference ID to navigate to. + */ + fun navigateTo(refID: String) { + try { + if (refID.isBlank()) { + Log.e("navigateTo", "Invalid refID: The reference ID is blank.") + } +// referenceId = refID + navigator.navigateTo(mContext, refID,object : RefIdDelegate { + override fun onRefByIDSuccess(PoiId: String?) { + Log.e("navigateTo", "PoiId is penguin view+++++++ $PoiId") + +// channelFlutter.invokeMethod( +// PenguinMethod.navigateToPOI.name, +// "navigateTo Success" +// ) + } + + override fun onGetByRefIDError(error: String?) { + Log.e("navigateTo", "error is penguin view+++++++ $error") + +// channelFlutter.invokeMethod( +// PenguinMethod.navigateToPOI.name, +// "navigateTo Failed: Invalid refID" +// ) + } + } , creationParams["clientID"] as String, creationParams["clientKey"] as String ) + + } catch (e: Exception) { + Log.e("navigateTo", "Exception occurred during navigation: ${e.message}", e) +// channelFlutter.invokeMethod( +// PenguinMethod.navigateToPOI.name, +// "Failed: Exception - ${e.message}" +// ) + } + } + + /** + * Called when Penguin UI setup is successful. + * + * @param warningCode Optional warning code received from the SDK. + */ + override fun onPenNavSuccess(warningCode: String?) { + val clinicId = creationParams["clinicID"] as String + + if(clinicId.isEmpty()) return + + navigateTo(clinicId) +// navigateTo("3-1") + } + + /** + * Called when there is an initialization error with Penguin UI. + * + * @param description Description of the error. + * @param errorType Type of initialization error. + */ + override fun onPenNavInitializationError( + description: String?, + errorType: InitializationErrorType? + ) { + val arguments: Map = mapOf( + "description" to description, + "type" to errorType?.name + ) + Log.d( + "description", + "description : ${description}" + ) + + channel.invokeMethod(PenguinMethod.onPenNavInitializationError.name, arguments) + Toast.makeText(mContext, "Navigation Error: $description", Toast.LENGTH_SHORT).show() + } + + /** + * Called when Penguin UI is dismissed. + */ + override fun onPenNavUIDismiss() { + // Handle UI dismissal if needed + try { + mContext.unregisterReceiver(permissionResultReceiver) + dispose(); + } catch (e: IllegalArgumentException) { + Log.e("PenguinView", "Receiver not registered: $e") + } + } + + override fun onReportIssue(issue: PIReportIssue?) { + TODO("Not yet implemented") + } + + override fun onSharedLocation(link: String?) { + TODO("Not yet implemented") + } + + override fun onLocationOffCampus(location: ArrayList?) { + TODO("Not yet implemented") + } + + override fun onLocationMessage(locationMessage: LocationMessage?) { + TODO("Not yet implemented") + } +} diff --git a/android/app/src/main/kotlin/com/cloudsolutions/HMGPatientApp/watch/samsung_watch/SamsungWatch.kt b/android/app/src/main/kotlin/com/cloudsolutions/HMGPatientApp/watch/samsung_watch/SamsungWatch.kt new file mode 100644 index 00000000..336651e4 --- /dev/null +++ b/android/app/src/main/kotlin/com/cloudsolutions/HMGPatientApp/watch/samsung_watch/SamsungWatch.kt @@ -0,0 +1,402 @@ +package com.cloudsolutions.HMGPatientApp.watch.samsung_watch + + + +import android.os.Build +import android.util.Log +import androidx.annotation.RequiresApi +import com.cloudsolutions.HMGPatientApp.MainActivity +import com.cloudsolutions.HMGPatientApp.watch.samsung_watch.model.Vitals +import io.flutter.embedding.engine.FlutterEngine +import io.flutter.plugin.common.MethodCall +import com.samsung.android.sdk.health.data.HealthDataService +import com.samsung.android.sdk.health.data.HealthDataStore +import com.samsung.android.sdk.health.data.data.AggregatedData +import com.samsung.android.sdk.health.data.data.HealthDataPoint +import com.samsung.android.sdk.health.data.permission.AccessType +import com.samsung.android.sdk.health.data.permission.Permission +import com.samsung.android.sdk.health.data.request.DataType +import com.samsung.android.sdk.health.data.request.DataTypes +import com.samsung.android.sdk.health.data.request.LocalTimeFilter +import com.samsung.android.sdk.health.data.request.LocalTimeGroup +import com.samsung.android.sdk.health.data.request.LocalTimeGroupUnit +import com.samsung.android.sdk.health.data.request.Ordering +import com.samsung.android.sdk.health.data.response.DataResponse +import io.flutter.plugin.common.MethodChannel +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch +import java.time.LocalDateTime +import java.time.LocalTime + +class SamsungWatch( + private var flutterEngine: FlutterEngine, + private var mainActivity: MainActivity +) { + + private lateinit var channel: MethodChannel + private lateinit var dataStore: HealthDataStore + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + private val TAG = "SamsungWatch" + + + private lateinit var vitals: MutableMap> + companion object { + private const val CHANNEL = "samsung_watch" + + } + init{ + create() + } + + @RequiresApi(Build.VERSION_CODES.O) + fun create() { + Log.d(TAG, "create: is called") +// openTok = OpenTok(mainActivity, flutterEngine) + channel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL) + channel.setMethodCallHandler { call: MethodCall, result: MethodChannel.Result -> + when (call.method) { + "init" -> { + Log.d(TAG, "onMethodCall: init called") + dataStore = HealthDataService.getStore(mainActivity) + vitals = mutableMapOf() + result.success("initialized") + } + + "getPermission"->{ + if(!this::dataStore.isInitialized) + result.error("DataStoreNotInitialized", "Please call init before requesting permissions", null) + val permSet = setOf( + Permission.of(DataTypes.HEART_RATE, AccessType.READ), + Permission.of(DataTypes.STEPS, AccessType.READ), + Permission.of(DataTypes.BLOOD_OXYGEN, AccessType.READ), + Permission.of(DataTypes.ACTIVITY_SUMMARY, AccessType.READ), + Permission.of(DataTypes.SLEEP, AccessType.READ), + Permission.of(DataTypes.BODY_TEMPERATURE, AccessType.READ), + Permission.of(DataTypes.EXERCISE, AccessType.READ), +// Permission.of(DataTypes.SKIN_TEMPERATURE, AccessType.READ), +// Permission.of(DataTypes.NUTRITION, AccessType.READ), + + ) + scope.launch { + try { + var granted = dataStore.getGrantedPermissions(permSet) + + if (granted.containsAll(permSet)) { + result.success("Permission Granted") + return@launch + } + + granted = dataStore.requestPermissions(permSet, mainActivity) + + if (granted.containsAll(permSet)) { + result.success("Permission Granted") // adapt result as needed + return@launch + } + result.error("PermissionError", "Permission Not Granted", null) // adapt result as needed + } catch (e: Exception) { + Log.e(TAG, "create: getPermission failed", e) + result.error("PermissionError", e.message, null) + } + } + } + + "getHeartRate"->{ + val dateTime = LocalDateTime.now().with(LocalTime.MIDNIGHT).minusDays(365) + val localTimeFilter = LocalTimeFilter.of(dateTime, LocalDateTime.now()) + val readRequest = DataTypes.HEART_RATE.readDataRequestBuilder + .setLocalTimeFilter(localTimeFilter) + .setOrdering(Ordering.DESC) + .build() + + scope.launch { + val heartRateList = dataStore.readData(readRequest).dataList + processHeartVital(heartRateList) + Log.d("TAG"," the data is ${vitals}") + print("the data is ${vitals}") + result.success("Data is obtained") + } + } + + + "getSleepData" -> { + val dateTime = LocalDateTime.now().with(LocalTime.MIDNIGHT).minusDays(365) + val localTimeFilter = LocalTimeFilter.of(dateTime, LocalDateTime.now()) + val readRequest = DataTypes.SLEEP.readDataRequestBuilder + .setLocalTimeFilter(localTimeFilter) + .setOrdering(Ordering.ASC) + .build() + scope.launch { + val sleepData = dataStore.readData(readRequest).dataList + processSleepVital(sleepData) + print("the data is $vitals") + Log.d(TAG, "the data is $vitals") + result.success("Data is obtained") + } + + } + + "steps"->{ + val dateTime = LocalDateTime.now().with(LocalTime.MIDNIGHT).minusDays(365) + val localTimeFilter = LocalTimeFilter.of(dateTime, LocalDateTime.now()) + val localTimeGroup = LocalTimeGroup.of(LocalTimeGroupUnit.HOURLY, 1) + val aggregateRequest = DataType.StepsType.TOTAL.requestBuilder + .setLocalTimeFilterWithGroup(localTimeFilter, localTimeGroup) + .setOrdering(Ordering.ASC) + .build() + + scope.launch { + val steps = dataStore.aggregateData(aggregateRequest) + processStepsCount(steps) + print("the data is $vitals") + Log.d(TAG, "the data is $vitals") + result.success("Data is obtained") + } + } + + "activitySummary"->{ + val dateTime = LocalDateTime.now().with(LocalTime.MIDNIGHT).minusDays(365) + val localTimeFilter = LocalTimeFilter.of(dateTime, LocalDateTime.now()) + val localTimeGroup = LocalTimeGroup.of(LocalTimeGroupUnit.HOURLY, 1) + val readRequest = DataType.ActivitySummaryType.TOTAL_ACTIVE_CALORIES_BURNED + .requestBuilder + .setLocalTimeFilterWithGroup(localTimeFilter, localTimeGroup) + .setOrdering(Ordering.DESC) + .build() + + scope.launch { + val activityResult = dataStore.aggregateData(readRequest).dataList + processActivity(activityResult) + Log.d("TAG"," the data is ${vitals}") + print("the data is ${vitals}") + result.success("Data is obtained") + } + +// val readRequest = DataTypes.EXERCISE.readDataRequestBuilder +// .setLocalTimeFilter(localTimeFilter) +// .build() +// +// scope.launch{ +// try { +// val readResult = dataStore.readData(readRequest) +// val dataPoints = readResult.dataList +// +// processActivity(dataPoints) +// +// +// } catch (e: Exception) { +// e.printStackTrace() +// } +// result.success("Data is obtained") +// } + } + + "bloodOxygen"->{ + val dateTime = LocalDateTime.now().with(LocalTime.MIDNIGHT).minusDays(365) + val localTimeFilter = LocalTimeFilter.of(dateTime, LocalDateTime.now()) + val readRequest = DataTypes.BLOOD_OXYGEN.readDataRequestBuilder + .setLocalTimeFilter(localTimeFilter) + .setOrdering(Ordering.DESC) + .build() + + scope.launch { + val bloodOxygenList = dataStore.readData(readRequest).dataList + processBloodOxygen(bloodOxygenList) + Log.d("TAG"," the data is ${vitals}") + print("the data is ${vitals["bloodOxygen"]}") + result.success("Data is obtained") + } + } + + + "bodyTemperature"->{ + val dateTime = LocalDateTime.now().with(LocalTime.MIDNIGHT).minusDays(365) + val localTimeFilter = LocalTimeFilter.of(dateTime, LocalDateTime.now()) + val readRequest = DataTypes.BODY_TEMPERATURE.readDataRequestBuilder + .setLocalTimeFilter(localTimeFilter) + .setOrdering(Ordering.DESC) + .build() + + scope.launch { + val bodyTemperatureList = dataStore.readData(readRequest).dataList + processBodyTemperature(bodyTemperatureList) + Log.d("TAG"," the data is ${vitals}") + print("the data is ${vitals["bodyTemperature"]}") + result.success("Data is obtained") + } + } + + "distance"->{ + val dateTime = LocalDateTime.now().with(LocalTime.MIDNIGHT).minusDays(365) + val localTimeFilter = LocalTimeFilter.of(dateTime, LocalDateTime.now()) + val localTimeGroup = LocalTimeGroup.of(LocalTimeGroupUnit.HOURLY, 1) + val readRequest = DataType.ActivitySummaryType.TOTAL_DISTANCE.requestBuilder + .setLocalTimeFilterWithGroup(localTimeFilter, localTimeGroup) + .setOrdering(Ordering.DESC) + .build() + + scope.launch { + val activityResult = dataStore.aggregateData(readRequest).dataList + processDistance(activityResult) + Log.d("TAG"," the data is ${vitals}") + print("the data is ${vitals}") + result.success("Data is obtained") + } + } + + "retrieveData"->{ + if(vitals.isEmpty()){ + result.error("NoDataFound", "No Data was obtained", null) + return@setMethodCallHandler + } + result.success(""" + { + "heartRate": ${vitals["heartRate"]}, + "steps": ${vitals["steps"]}, + "sleep": ${vitals["sleep"]}, + "activity": ${vitals["activity"]}, + "bloodOxygen": ${vitals["bloodOxygen"]}, + "bodyTemperature": ${vitals["bodyTemperature"]}, + "distance": ${vitals["distance"]} + } + """.trimIndent()) + } + + + "closeCoroutineScope"->{ + destroy() + result.success("Coroutine Scope Cancelled") + } + + else -> { + result.notImplemented() + } + } + } + } + + private fun CoroutineScope.processDistance(activityResult: List>) { + vitals["distance"] = mutableListOf() + activityResult.forEach { stepData -> + val vitalData = Vitals().apply { + + value = stepData.value.toString() + timeStamp = stepData.startTime.toString() + } + (vitals["distance"] as MutableList).add(vitalData) + } + } + + private fun CoroutineScope.processBodyTemperature( bodyTemperatureList :List) { + vitals["bodyTemperature"] = mutableListOf() + bodyTemperatureList.forEach { stepData -> + val vitalData = Vitals().apply { + value = stepData.getValue(DataType.BodyTemperatureType.BODY_TEMPERATURE).toString() + timeStamp = stepData.endTime.toString() + } + (vitals["bodyTemperature"] as MutableList).add(vitalData) + } + } + + private fun CoroutineScope.processBloodOxygen( bloodOxygenList :List) { + vitals["bloodOxygen"] = mutableListOf() + bloodOxygenList.forEach { stepData -> + val vitalData = Vitals().apply { + value = stepData.getValue(DataType.BloodOxygenType.OXYGEN_SATURATION).toString() + timeStamp = stepData.endTime.toString() + } + (vitals["bloodOxygen"] as MutableList).add(vitalData) + } + } + + +// private fun CoroutineScope.processActivity(activityResult: List>) { +// +// vitals["activity"] = mutableListOf() +// activityResult.forEach { stepData -> +// val vitalData = Vitals().apply { +// +// value = stepData.value.toString() +// timeStamp = stepData.startTime.toString() +// } +// (vitals["activity"] as MutableList).add(vitalData) +// } +// } + private fun CoroutineScope.processActivity(activityResult: List>) { + + vitals["activity"] = mutableListOf() + activityResult.forEach { stepData -> + val vitalData = Vitals().apply { + + value = stepData.value.toString() + timeStamp = stepData.startTime.toString() + } + (vitals["activity"] as MutableList).add(vitalData) + } + +// dataPoints.forEach { dataPoint -> +// val sessions = dataPoint.getValue(DataType.ExerciseType.SESSIONS) +// +// sessions?.forEach { session -> +// +// val exerciseSessionCalories = session.calories +// val vitalData = Vitals().apply { +// value = exerciseSessionCalories.toString() +// timeStamp = session.startTime.toString() +// } +// (vitals["activity"] as MutableList).add(vitalData) +// } +// } + } + + private fun CoroutineScope.processStepsCount(result: DataResponse>) { + val stepCount = ArrayList>() + var totalSteps: Long = 0 + vitals["steps"] = mutableListOf() + result.dataList.forEach { stepData -> + val vitalData = Vitals().apply { + value = (stepData.value as Long).toString() + timeStamp = stepData.startTime.toString() + } + (vitals["steps"] as MutableList).add(vitalData) + } + + } + + private fun CoroutineScope.processSleepVital(sleepData: List) { + vitals["sleep"] = mutableListOf() + sleepData.forEach { + (vitals["sleep"] as MutableList).add( + Vitals().apply { + timeStamp = it.startTime.toString() + value = (it.getValue(DataType.SleepType.DURATION)?.toMillis().toString()) + } + ) + } + } + + private suspend fun CoroutineScope.processHeartVital( + heartRateList: List, + ) { + vitals["heartRate"] = mutableListOf() + heartRateList.forEach { + (vitals["heartRate"] as MutableList).add(processHeartRateData(it)) + } + } + + private fun processHeartRateData(heartRateData: HealthDataPoint) = + Vitals().apply { + heartRateData.getValue(DataType.HeartRateType.MAX_HEART_RATE)?.let { + value = it.toString() + } + timeStamp = heartRateData.startTime.toString() + } + + + fun destroy() { + scope.cancel() + } + +} diff --git a/android/app/src/main/kotlin/com/cloudsolutions/HMGPatientApp/watch/samsung_watch/model/Vitals.kt b/android/app/src/main/kotlin/com/cloudsolutions/HMGPatientApp/watch/samsung_watch/model/Vitals.kt new file mode 100644 index 00000000..577ab283 --- /dev/null +++ b/android/app/src/main/kotlin/com/cloudsolutions/HMGPatientApp/watch/samsung_watch/model/Vitals.kt @@ -0,0 +1,13 @@ +package com.cloudsolutions.HMGPatientApp.watch.samsung_watch.model + +data class Vitals( + var value : String = "", + var timeStamp :String = "" +){ + override fun toString(): String { + return """{ + "value": "$value", + "timeStamp": "$timeStamp"} + """.trimIndent() + } +} \ No newline at end of file diff --git a/android/app/src/main2/AndroidManifest.xml b/android/app/src/main2/AndroidManifest.xml new file mode 100644 index 00000000..e07739b9 --- /dev/null +++ b/android/app/src/main2/AndroidManifest.xml @@ -0,0 +1,274 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main2/kotlin/com/ejada/hmg/MainActivity.kt b/android/app/src/main2/kotlin/com/ejada/hmg/MainActivity.kt new file mode 100644 index 00000000..ece584b2 --- /dev/null +++ b/android/app/src/main2/kotlin/com/ejada/hmg/MainActivity.kt @@ -0,0 +1,84 @@ +package com.ejada.hmg + +import android.content.Intent +import android.content.pm.PackageManager +import android.os.Build +import android.util.Log +import android.view.WindowManager +import androidx.annotation.NonNull +import androidx.annotation.Nullable +import androidx.annotation.RequiresApi +import com.ejada.hmg.penguin.PenguinInPlatformBridge +import com.ejada.hmg.watch.huawei.HuaweiWatch +import com.ejada.hmg.watch.huawei.samsung_watch.SamsungWatch +import com.huawei.hms.hihealth.result.HealthKitAuthResult +import com.huawei.hms.support.api.entity.auth.Scope +import io.flutter.embedding.android.FlutterFragmentActivity +import io.flutter.embedding.engine.FlutterEngine +import io.flutter.plugins.GeneratedPluginRegistrant + + +class MainActivity: FlutterFragmentActivity() { + + private var huaweiWatch : HuaweiWatch? = null + @RequiresApi(Build.VERSION_CODES.O) + override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) { + GeneratedPluginRegistrant.registerWith(flutterEngine); + // Create Flutter Platform Bridge + this.window.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON or WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD or WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON) + + PenguinInPlatformBridge(flutterEngine, this).create() + SamsungWatch(flutterEngine, this) + huaweiWatch = HuaweiWatch(flutterEngine, this) + } + + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + + val granted = grantResults.all { it == PackageManager.PERMISSION_GRANTED } + val intent = Intent("PERMISSION_RESULT_ACTION").apply { + putExtra("PERMISSION_GRANTED", granted) + } + sendBroadcast(intent) + + // Log the request code and permission results + Log.d("PermissionsResult", "Request Code: $requestCode") + Log.d("PermissionsResult", "Permissions: ${permissions.joinToString()}") + Log.d("PermissionsResult", "Grant Results: ${grantResults.joinToString()}") + + } + + override fun onResume() { + super.onResume() + } + +// override fun onActivityResult(requestCode: Int, resultCode: Int, @Nullable data: Intent?) { +// super.onActivityResult(requestCode, resultCode, data) +// +// // Process only the response result of the authorization process. +// if (requestCode == 1002) { +// // Obtain the authorization response result from the intent. +// val result: HealthKitAuthResult? = huaweiWatch?.mSettingController?.parseHealthKitAuthResultFromIntent(data) +// if (result == null) { +// Log.w(huaweiWatch?.TAG, "authorization fail") +// return +// } +// +// if (result.isSuccess) { +// Log.i(huaweiWatch?.TAG, "authorization success") +// if (result.getAuthAccount() != null && result.authAccount.authorizedScopes != null) { +// val authorizedScopes: MutableSet = result.authAccount.authorizedScopes +// if(authorizedScopes.isNotEmpty()) { +// huaweiWatch?.getHealthAppAuthorization() +// } +// } +// } else { +// Log.w("MainActivty", "authorization fail, errorCode:" + result.getErrorCode()) +// } +// } +// } +} diff --git a/android/app/src/main2/kotlin/com/ejada/hmg/watch/samsung_watch/SamsungWatch.kt b/android/app/src/main2/kotlin/com/ejada/hmg/watch/samsung_watch/SamsungWatch.kt new file mode 100644 index 00000000..09aafff2 --- /dev/null +++ b/android/app/src/main2/kotlin/com/ejada/hmg/watch/samsung_watch/SamsungWatch.kt @@ -0,0 +1,402 @@ +package com.ejada.hmg.watch.huawei.samsung_watch + + + +import com.ejada.hmg.MainActivity +import android.os.Build +import android.util.Log +import androidx.annotation.RequiresApi +import io.flutter.embedding.engine.FlutterEngine +import io.flutter.plugin.common.MethodCall +import com.ejada.hmg.watch.huawei.samsung_watch.model.Vitals +import com.samsung.android.sdk.health.data.HealthDataService +import com.samsung.android.sdk.health.data.HealthDataStore +import com.samsung.android.sdk.health.data.data.AggregatedData +import com.samsung.android.sdk.health.data.data.HealthDataPoint +import com.samsung.android.sdk.health.data.permission.AccessType +import com.samsung.android.sdk.health.data.permission.Permission +import com.samsung.android.sdk.health.data.request.DataType +import com.samsung.android.sdk.health.data.request.DataTypes +import com.samsung.android.sdk.health.data.request.LocalTimeFilter +import com.samsung.android.sdk.health.data.request.LocalTimeGroup +import com.samsung.android.sdk.health.data.request.LocalTimeGroupUnit +import com.samsung.android.sdk.health.data.request.Ordering +import com.samsung.android.sdk.health.data.response.DataResponse +import io.flutter.plugin.common.MethodChannel +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch +import java.time.LocalDateTime +import java.time.LocalTime + +class SamsungWatch( + private var flutterEngine: FlutterEngine, + private var mainActivity: MainActivity +) { + + private lateinit var channel: MethodChannel + private lateinit var dataStore: HealthDataStore + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + private val TAG = "SamsungWatch" + + + private lateinit var vitals: MutableMap> + companion object { + private const val CHANNEL = "samsung_watch" + + } + init{ + create() + } + + @RequiresApi(Build.VERSION_CODES.O) + fun create() { + Log.d(TAG, "create: is called") +// openTok = OpenTok(mainActivity, flutterEngine) + channel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL) + channel.setMethodCallHandler { call: MethodCall, result: MethodChannel.Result -> + when (call.method) { + "init" -> { + Log.d(TAG, "onMethodCall: init called") + dataStore = HealthDataService.getStore(mainActivity) + vitals = mutableMapOf() + result.success("initialized") + } + + "getPermission"->{ + if(!this::dataStore.isInitialized) + result.error("DataStoreNotInitialized", "Please call init before requesting permissions", null) + val permSet = setOf( + Permission.of(DataTypes.HEART_RATE, AccessType.READ), + Permission.of(DataTypes.STEPS, AccessType.READ), + Permission.of(DataTypes.BLOOD_OXYGEN, AccessType.READ), + Permission.of(DataTypes.ACTIVITY_SUMMARY, AccessType.READ), + Permission.of(DataTypes.SLEEP, AccessType.READ), + Permission.of(DataTypes.BODY_TEMPERATURE, AccessType.READ), + Permission.of(DataTypes.EXERCISE, AccessType.READ), +// Permission.of(DataTypes.SKIN_TEMPERATURE, AccessType.READ), +// Permission.of(DataTypes.NUTRITION, AccessType.READ), + + ) + scope.launch { + try { + var granted = dataStore.getGrantedPermissions(permSet) + + if (granted.containsAll(permSet)) { + result.success("Permission Granted") + return@launch + } + + granted = dataStore.requestPermissions(permSet, mainActivity) + + if (granted.containsAll(permSet)) { + result.success("Permission Granted") // adapt result as needed + return@launch + } + result.error("PermissionError", "Permission Not Granted", null) // adapt result as needed + } catch (e: Exception) { + Log.e(TAG, "create: getPermission failed", e) + result.error("PermissionError", e.message, null) + } + } + } + + "getHeartRate"->{ + val dateTime = LocalDateTime.now().with(LocalTime.MIDNIGHT).minusDays(365) + val localTimeFilter = LocalTimeFilter.of(dateTime, LocalDateTime.now()) + val readRequest = DataTypes.HEART_RATE.readDataRequestBuilder + .setLocalTimeFilter(localTimeFilter) + .setOrdering(Ordering.DESC) + .build() + + scope.launch { + val heartRateList = dataStore.readData(readRequest).dataList + processHeartVital(heartRateList) + Log.d("TAG"," the data is ${vitals}") + print("the data is ${vitals}") + result.success("Data is obtained") + } + } + + + "getSleepData" -> { + val dateTime = LocalDateTime.now().with(LocalTime.MIDNIGHT).minusDays(365) + val localTimeFilter = LocalTimeFilter.of(dateTime, LocalDateTime.now()) + val readRequest = DataTypes.SLEEP.readDataRequestBuilder + .setLocalTimeFilter(localTimeFilter) + .setOrdering(Ordering.ASC) + .build() + scope.launch { + val sleepData = dataStore.readData(readRequest).dataList + processSleepVital(sleepData) + print("the data is $vitals") + Log.d(TAG, "the data is $vitals") + result.success("Data is obtained") + } + + } + + "steps"->{ + val dateTime = LocalDateTime.now().with(LocalTime.MIDNIGHT).minusDays(365) + val localTimeFilter = LocalTimeFilter.of(dateTime, LocalDateTime.now()) + val localTimeGroup = LocalTimeGroup.of(LocalTimeGroupUnit.HOURLY, 1) + val aggregateRequest = DataType.StepsType.TOTAL.requestBuilder + .setLocalTimeFilterWithGroup(localTimeFilter, localTimeGroup) + .setOrdering(Ordering.ASC) + .build() + + scope.launch { + val steps = dataStore.aggregateData(aggregateRequest) + processStepsCount(steps) + print("the data is $vitals") + Log.d(TAG, "the data is $vitals") + result.success("Data is obtained") + } + } + + "activitySummary"->{ + val dateTime = LocalDateTime.now().with(LocalTime.MIDNIGHT).minusDays(365) + val localTimeFilter = LocalTimeFilter.of(dateTime, LocalDateTime.now()) + val localTimeGroup = LocalTimeGroup.of(LocalTimeGroupUnit.HOURLY, 1) + val readRequest = DataType.ActivitySummaryType.TOTAL_ACTIVE_CALORIES_BURNED + .requestBuilder + .setLocalTimeFilterWithGroup(localTimeFilter, localTimeGroup) + .setOrdering(Ordering.DESC) + .build() + + scope.launch { + val activityResult = dataStore.aggregateData(readRequest).dataList + processActivity(activityResult) + Log.d("TAG"," the data is ${vitals}") + print("the data is ${vitals}") + result.success("Data is obtained") + } + +// val readRequest = DataTypes.EXERCISE.readDataRequestBuilder +// .setLocalTimeFilter(localTimeFilter) +// .build() +// +// scope.launch{ +// try { +// val readResult = dataStore.readData(readRequest) +// val dataPoints = readResult.dataList +// +// processActivity(dataPoints) +// +// +// } catch (e: Exception) { +// e.printStackTrace() +// } +// result.success("Data is obtained") +// } + } + + "bloodOxygen"->{ + val dateTime = LocalDateTime.now().with(LocalTime.MIDNIGHT).minusDays(365) + val localTimeFilter = LocalTimeFilter.of(dateTime, LocalDateTime.now()) + val readRequest = DataTypes.BLOOD_OXYGEN.readDataRequestBuilder + .setLocalTimeFilter(localTimeFilter) + .setOrdering(Ordering.DESC) + .build() + + scope.launch { + val bloodOxygenList = dataStore.readData(readRequest).dataList + processBloodOxygen(bloodOxygenList) + Log.d("TAG"," the data is ${vitals}") + print("the data is ${vitals["bloodOxygen"]}") + result.success("Data is obtained") + } + } + + + "bodyTemperature"->{ + val dateTime = LocalDateTime.now().with(LocalTime.MIDNIGHT).minusDays(365) + val localTimeFilter = LocalTimeFilter.of(dateTime, LocalDateTime.now()) + val readRequest = DataTypes.BODY_TEMPERATURE.readDataRequestBuilder + .setLocalTimeFilter(localTimeFilter) + .setOrdering(Ordering.DESC) + .build() + + scope.launch { + val bodyTemperatureList = dataStore.readData(readRequest).dataList + processBodyTemperature(bodyTemperatureList) + Log.d("TAG"," the data is ${vitals}") + print("the data is ${vitals["bodyTemperature"]}") + result.success("Data is obtained") + } + } + + "distance"->{ + val dateTime = LocalDateTime.now().with(LocalTime.MIDNIGHT).minusDays(365) + val localTimeFilter = LocalTimeFilter.of(dateTime, LocalDateTime.now()) + val localTimeGroup = LocalTimeGroup.of(LocalTimeGroupUnit.HOURLY, 1) + val readRequest = DataType.ActivitySummaryType.TOTAL_DISTANCE.requestBuilder + .setLocalTimeFilterWithGroup(localTimeFilter, localTimeGroup) + .setOrdering(Ordering.DESC) + .build() + + scope.launch { + val activityResult = dataStore.aggregateData(readRequest).dataList + processDistance(activityResult) + Log.d("TAG"," the data is ${vitals}") + print("the data is ${vitals}") + result.success("Data is obtained") + } + } + + "retrieveData"->{ + if(vitals.isEmpty()){ + result.error("NoDataFound", "No Data was obtained", null) + return@setMethodCallHandler + } + result.success(""" + { + "heartRate": ${vitals["heartRate"]}, + "steps": ${vitals["steps"]}, + "sleep": ${vitals["sleep"]}, + "activity": ${vitals["activity"]}, + "bloodOxygen": ${vitals["bloodOxygen"]}, + "bodyTemperature": ${vitals["bodyTemperature"]}, + "distance": ${vitals["distance"]} + } + """.trimIndent()) + } + + + "closeCoroutineScope"->{ + destroy() + result.success("Coroutine Scope Cancelled") + } + + else -> { + result.notImplemented() + } + } + } + } + + private fun CoroutineScope.processDistance(activityResult: List>) { + vitals["distance"] = mutableListOf() + activityResult.forEach { stepData -> + val vitalData = Vitals().apply { + + value = stepData.value.toString() + timeStamp = stepData.startTime.toString() + } + (vitals["distance"] as MutableList).add(vitalData) + } + } + + private fun CoroutineScope.processBodyTemperature( bodyTemperatureList :List) { + vitals["bodyTemperature"] = mutableListOf() + bodyTemperatureList.forEach { stepData -> + val vitalData = Vitals().apply { + value = stepData.getValue(DataType.BodyTemperatureType.BODY_TEMPERATURE).toString() + timeStamp = stepData.endTime.toString() + } + (vitals["bodyTemperature"] as MutableList).add(vitalData) + } + } + + private fun CoroutineScope.processBloodOxygen( bloodOxygenList :List) { + vitals["bloodOxygen"] = mutableListOf() + bloodOxygenList.forEach { stepData -> + val vitalData = Vitals().apply { + value = stepData.getValue(DataType.BloodOxygenType.OXYGEN_SATURATION).toString() + timeStamp = stepData.endTime.toString() + } + (vitals["bloodOxygen"] as MutableList).add(vitalData) + } + } + + +// private fun CoroutineScope.processActivity(activityResult: List>) { +// +// vitals["activity"] = mutableListOf() +// activityResult.forEach { stepData -> +// val vitalData = Vitals().apply { +// +// value = stepData.value.toString() +// timeStamp = stepData.startTime.toString() +// } +// (vitals["activity"] as MutableList).add(vitalData) +// } +// } + private fun CoroutineScope.processActivity(activityResult: List>) { + + vitals["activity"] = mutableListOf() + activityResult.forEach { stepData -> + val vitalData = Vitals().apply { + + value = stepData.value.toString() + timeStamp = stepData.startTime.toString() + } + (vitals["activity"] as MutableList).add(vitalData) + } + +// dataPoints.forEach { dataPoint -> +// val sessions = dataPoint.getValue(DataType.ExerciseType.SESSIONS) +// +// sessions?.forEach { session -> +// +// val exerciseSessionCalories = session.calories +// val vitalData = Vitals().apply { +// value = exerciseSessionCalories.toString() +// timeStamp = session.startTime.toString() +// } +// (vitals["activity"] as MutableList).add(vitalData) +// } +// } + } + + private fun CoroutineScope.processStepsCount(result: DataResponse>) { + val stepCount = ArrayList>() + var totalSteps: Long = 0 + vitals["steps"] = mutableListOf() + result.dataList.forEach { stepData -> + val vitalData = Vitals().apply { + value = (stepData.value as Long).toString() + timeStamp = stepData.startTime.toString() + } + (vitals["steps"] as MutableList).add(vitalData) + } + + } + + private fun CoroutineScope.processSleepVital(sleepData: List) { + vitals["sleep"] = mutableListOf() + sleepData.forEach { + (vitals["sleep"] as MutableList).add( + Vitals().apply { + timeStamp = it.startTime.toString() + value = (it.getValue(DataType.SleepType.DURATION)?.toMillis().toString()) + } + ) + } + } + + private suspend fun CoroutineScope.processHeartVital( + heartRateList: List, + ) { + vitals["heartRate"] = mutableListOf() + heartRateList.forEach { + (vitals["heartRate"] as MutableList).add(processHeartRateData(it)) + } + } + + private fun processHeartRateData(heartRateData: HealthDataPoint) = + Vitals().apply { + heartRateData.getValue(DataType.HeartRateType.MAX_HEART_RATE)?.let { + value = it.toString() + } + timeStamp = heartRateData.startTime.toString() + } + + + fun destroy() { + scope.cancel() + } + +} diff --git a/android/app/src/main2/kotlin/com/ejada/hmg/watch/samsung_watch/model/Vitals.kt b/android/app/src/main2/kotlin/com/ejada/hmg/watch/samsung_watch/model/Vitals.kt new file mode 100644 index 00000000..3b5cdfe4 --- /dev/null +++ b/android/app/src/main2/kotlin/com/ejada/hmg/watch/samsung_watch/model/Vitals.kt @@ -0,0 +1,13 @@ +package com.ejada.hmg.watch.huawei.samsung_watch.model + +data class Vitals( + var value : String = "", + var timeStamp :String = "" +){ + override fun toString(): String { + return """{ + "value": "$value", + "timeStamp": "$timeStamp"} + """.trimIndent() + } +} \ No newline at end of file diff --git a/android/app/src/main2/res/drawable-v21/launch_background.xml b/android/app/src/main2/res/drawable-v21/launch_background.xml new file mode 100644 index 00000000..f74085f3 --- /dev/null +++ b/android/app/src/main2/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/android/app/src/main2/res/drawable/app_icon.png b/android/app/src/main2/res/drawable/app_icon.png new file mode 100755 index 00000000..2d394f83 Binary files /dev/null and b/android/app/src/main2/res/drawable/app_icon.png differ diff --git a/android/app/src/main2/res/drawable/food.png b/android/app/src/main2/res/drawable/food.png new file mode 100644 index 00000000..41b394d3 Binary files /dev/null and b/android/app/src/main2/res/drawable/food.png differ diff --git a/android/app/src/main2/res/drawable/launch_background.xml b/android/app/src/main2/res/drawable/launch_background.xml new file mode 100644 index 00000000..304732f8 --- /dev/null +++ b/android/app/src/main2/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/android/app/src/main2/res/drawable/me.png b/android/app/src/main2/res/drawable/me.png new file mode 100644 index 00000000..ba75bc55 Binary files /dev/null and b/android/app/src/main2/res/drawable/me.png differ diff --git a/android/app/src/main2/res/drawable/sample_large_icon.png b/android/app/src/main2/res/drawable/sample_large_icon.png new file mode 100644 index 00000000..f354ca23 Binary files /dev/null and b/android/app/src/main2/res/drawable/sample_large_icon.png differ diff --git a/android/app/src/main2/res/drawable/secondary_icon.png b/android/app/src/main2/res/drawable/secondary_icon.png new file mode 100644 index 00000000..9de9ff41 Binary files /dev/null and b/android/app/src/main2/res/drawable/secondary_icon.png differ diff --git a/android/app/src/main2/res/layout/activity_whats_app_code.xml b/android/app/src/main2/res/layout/activity_whats_app_code.xml new file mode 100644 index 00000000..3cd824c9 --- /dev/null +++ b/android/app/src/main2/res/layout/activity_whats_app_code.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/android/app/src/main2/res/layout/local_video.xml b/android/app/src/main2/res/layout/local_video.xml new file mode 100644 index 00000000..f47c48cd --- /dev/null +++ b/android/app/src/main2/res/layout/local_video.xml @@ -0,0 +1,14 @@ + + + + + \ No newline at end of file diff --git a/android/app/src/main2/res/layout/remote_video.xml b/android/app/src/main2/res/layout/remote_video.xml new file mode 100644 index 00000000..cfdbeb0d --- /dev/null +++ b/android/app/src/main2/res/layout/remote_video.xml @@ -0,0 +1,20 @@ + + + + + + + \ No newline at end of file diff --git a/package/device_calendar_plus/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main2/res/mipmap-hdpi/ic_launcher.png similarity index 100% rename from package/device_calendar_plus/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png rename to android/app/src/main2/res/mipmap-hdpi/ic_launcher.png diff --git a/android/app/src/main2/res/mipmap-hdpi/ic_launcher_local.png b/android/app/src/main2/res/mipmap-hdpi/ic_launcher_local.png new file mode 100644 index 00000000..348b5116 Binary files /dev/null and b/android/app/src/main2/res/mipmap-hdpi/ic_launcher_local.png differ diff --git a/package/device_calendar_plus/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main2/res/mipmap-mdpi/ic_launcher.png similarity index 100% rename from package/device_calendar_plus/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png rename to android/app/src/main2/res/mipmap-mdpi/ic_launcher.png diff --git a/android/app/src/main2/res/mipmap-mdpi/ic_launcher_local.png b/android/app/src/main2/res/mipmap-mdpi/ic_launcher_local.png new file mode 100644 index 00000000..410b1b1e Binary files /dev/null and b/android/app/src/main2/res/mipmap-mdpi/ic_launcher_local.png differ diff --git a/package/device_calendar_plus/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main2/res/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from package/device_calendar_plus/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png rename to android/app/src/main2/res/mipmap-xhdpi/ic_launcher.png diff --git a/android/app/src/main2/res/mipmap-xhdpi/ic_launcher_local.png b/android/app/src/main2/res/mipmap-xhdpi/ic_launcher_local.png new file mode 100644 index 00000000..bb9943af Binary files /dev/null and b/android/app/src/main2/res/mipmap-xhdpi/ic_launcher_local.png differ diff --git a/package/device_calendar_plus/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main2/res/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from package/device_calendar_plus/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png rename to android/app/src/main2/res/mipmap-xxhdpi/ic_launcher.png diff --git a/android/app/src/main2/res/mipmap-xxhdpi/ic_launcher_local.png b/android/app/src/main2/res/mipmap-xxhdpi/ic_launcher_local.png new file mode 100644 index 00000000..0b9d9359 Binary files /dev/null and b/android/app/src/main2/res/mipmap-xxhdpi/ic_launcher_local.png differ diff --git a/package/device_calendar_plus/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main2/res/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from package/device_calendar_plus/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png rename to android/app/src/main2/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/android/app/src/main2/res/mipmap-xxxhdpi/ic_launcher_local.png b/android/app/src/main2/res/mipmap-xxxhdpi/ic_launcher_local.png new file mode 100644 index 00000000..aaa9808d Binary files /dev/null and b/android/app/src/main2/res/mipmap-xxxhdpi/ic_launcher_local.png differ diff --git a/android/app/src/main2/res/raw/keep.xml b/android/app/src/main2/res/raw/keep.xml new file mode 100644 index 00000000..944a7ace --- /dev/null +++ b/android/app/src/main2/res/raw/keep.xml @@ -0,0 +1,3 @@ + + \ No newline at end of file diff --git a/android/app/src/main2/res/raw/slow_spring_board.mp3 b/android/app/src/main2/res/raw/slow_spring_board.mp3 new file mode 100644 index 00000000..60dbf979 Binary files /dev/null and b/android/app/src/main2/res/raw/slow_spring_board.mp3 differ diff --git a/android/app/src/main2/res/values-night/styles.xml b/android/app/src/main2/res/values-night/styles.xml new file mode 100644 index 00000000..06952be7 --- /dev/null +++ b/android/app/src/main2/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/android/app/src/main2/res/values/mapbox_access_token.xml b/android/app/src/main2/res/values/mapbox_access_token.xml new file mode 100644 index 00000000..65bc4b37 --- /dev/null +++ b/android/app/src/main2/res/values/mapbox_access_token.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/android/app/src/main2/res/values/strings.xml b/android/app/src/main2/res/values/strings.xml new file mode 100644 index 00000000..2d103337 --- /dev/null +++ b/android/app/src/main2/res/values/strings.xml @@ -0,0 +1,23 @@ + + HMG Patient App + + + Unknown error: the Geofence service is not available now. + + + Geofence service is not available now. Go to Settings>Location>Mode and choose High accuracy. + + + Your app has registered too many geofences. + + + You have provided too many PendingIntents to the addGeofences() call. + + + App do not have permission to access location service. + + + Geofence requests happened too frequently. + + pk.eyJ1IjoicndhaWQiLCJhIjoiY2x5cGo4aHNjMGNsbTJyc2djempobGQxaSJ9.RCaC6WrUt4A4YnZNfxnONQ + diff --git a/android/app/src/main2/res/values/styles.xml b/android/app/src/main2/res/values/styles.xml new file mode 100644 index 00000000..1f83a33f --- /dev/null +++ b/android/app/src/main2/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/android/gradle.properties b/android/gradle.properties index f018a618..647291d1 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,3 +1,10 @@ -org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError -android.useAndroidX=true +#org.gradle.jvmargs=-xmx4608m +android.enableR8=true android.enableJetifier=true +android.useDeprecatedNdk=true +org.gradle.jvmargs=-Xmx4096m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 +org.gradle.daemon=true +org.gradle.parallel=true +org.gradle.configureondemand=true +android.useAndroidX=true +android.enableImpeller=false diff --git a/assets/images/png/Carparking_services_image.png b/assets/images/png/Carparking_services_image.png new file mode 100644 index 00000000..9ce08c78 Binary files /dev/null and b/assets/images/png/Carparking_services_image.png differ diff --git a/assets/images/png/blood_donation_image.png b/assets/images/png/blood_donation_image.png new file mode 100644 index 00000000..c7e82c7f Binary files /dev/null and b/assets/images/png/blood_donation_image.png differ diff --git a/assets/images/png/cmc_services_image.png b/assets/images/png/cmc_services_image.png new file mode 100644 index 00000000..baad76de Binary files /dev/null and b/assets/images/png/cmc_services_image.png differ diff --git a/assets/images/png/emergency_services_image.png b/assets/images/png/emergency_services_image.png new file mode 100644 index 00000000..f9924a52 Binary files /dev/null and b/assets/images/png/emergency_services_image.png differ diff --git a/assets/images/png/ereferral_services_image.png b/assets/images/png/ereferral_services_image.png new file mode 100644 index 00000000..10d0d14a Binary files /dev/null and b/assets/images/png/ereferral_services_image.png differ diff --git a/assets/images/png/healthtrackers_services_image.png b/assets/images/png/healthtrackers_services_image.png new file mode 100644 index 00000000..2d0c3f8a Binary files /dev/null and b/assets/images/png/healthtrackers_services_image.png differ diff --git a/assets/images/png/livechat_services_image.png b/assets/images/png/livechat_services_image.png new file mode 100644 index 00000000..7e33a933 Binary files /dev/null and b/assets/images/png/livechat_services_image.png differ diff --git a/assets/images/png/smartwatch_services_image.png b/assets/images/png/smartwatch_services_image.png new file mode 100644 index 00000000..303737bf Binary files /dev/null and b/assets/images/png/smartwatch_services_image.png differ diff --git a/assets/images/png/water_consumption_image.png b/assets/images/png/water_consumption_image.png new file mode 100644 index 00000000..604fa8a2 Binary files /dev/null and b/assets/images/png/water_consumption_image.png differ diff --git a/assets/images/svg/bluetooth.svg b/assets/images/svg/bluetooth.svg new file mode 100644 index 00000000..5fe2cf0c --- /dev/null +++ b/assets/images/svg/bluetooth.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/images/svg/watch_activity.svg b/assets/images/svg/watch_activity.svg new file mode 100644 index 00000000..8da915fa --- /dev/null +++ b/assets/images/svg/watch_activity.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/svg/watch_activity_trailing.svg b/assets/images/svg/watch_activity_trailing.svg new file mode 100644 index 00000000..dc09c058 --- /dev/null +++ b/assets/images/svg/watch_activity_trailing.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/images/svg/watch_bmi.svg b/assets/images/svg/watch_bmi.svg new file mode 100644 index 00000000..4d34d5d0 --- /dev/null +++ b/assets/images/svg/watch_bmi.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/images/svg/watch_bmi_trailing.svg b/assets/images/svg/watch_bmi_trailing.svg new file mode 100644 index 00000000..772d18a4 --- /dev/null +++ b/assets/images/svg/watch_bmi_trailing.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/svg/watch_height.svg b/assets/images/svg/watch_height.svg new file mode 100644 index 00000000..e70b4b89 --- /dev/null +++ b/assets/images/svg/watch_height.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/svg/watch_sleep.svg b/assets/images/svg/watch_sleep.svg new file mode 100644 index 00000000..dd21f628 --- /dev/null +++ b/assets/images/svg/watch_sleep.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/svg/watch_sleep_trailing.svg b/assets/images/svg/watch_sleep_trailing.svg new file mode 100644 index 00000000..dff5ab36 --- /dev/null +++ b/assets/images/svg/watch_sleep_trailing.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/images/svg/watch_steps.svg b/assets/images/svg/watch_steps.svg new file mode 100644 index 00000000..730c1892 --- /dev/null +++ b/assets/images/svg/watch_steps.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/images/svg/watch_steps_trailing.svg b/assets/images/svg/watch_steps_trailing.svg new file mode 100644 index 00000000..2c1b2f20 --- /dev/null +++ b/assets/images/svg/watch_steps_trailing.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/assets/images/svg/watch_weight.svg b/assets/images/svg/watch_weight.svg new file mode 100644 index 00000000..3acddca5 --- /dev/null +++ b/assets/images/svg/watch_weight.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/svg/watch_weight_trailing.svg b/assets/images/svg/watch_weight_trailing.svg new file mode 100644 index 00000000..3e2b3dd8 --- /dev/null +++ b/assets/images/svg/watch_weight_trailing.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/assets/langs/ar-SA.json b/assets/langs/ar-SA.json index 765abc78..f9cc3035 100644 --- a/assets/langs/ar-SA.json +++ b/assets/langs/ar-SA.json @@ -3,6 +3,8 @@ "arabic": "العربية", "login": "تسجيل الدخول", "noDataAvailable": "لا توجد بيانات متاحة", + "noRatingAvailable": "لا يوجد تقييم متاح", + "doctorDoesNotHaveRating": "لم يحصل هذا الطبيب على أي تقييمات بعد.", "confirm": "تأكيد", "loadingText": "جاري التحميل، يرجى الانتظار...", "kilometerUnit": "كم", @@ -187,7 +189,7 @@ "lastName": "اسم العائلة", "female": "أنثى", "male": "ذكر", - "preferredLanguage": "اللغة المفضلة *", + "preferredLanguage": "اللغة المفضلة", "locationsRegister": "أين ترغب في إنشاء هذا الملف؟", "ksa": "السعودية", "dubai": "دبي", @@ -301,7 +303,7 @@ "vitalSignsSubTitle": "التقارير", "myMedical": "نشط", "myMedicalSubtitle": "الأدوية", - "myDoctor": "طبيبي", + "myDoctor": "أطبائي", "myDoctorSubtitle": "القائمة", "eye": "العين", "eyeSubtitle": "القياس", @@ -363,7 +365,7 @@ "paymentOnline": "الخدمة", "onlineCheckIn": "تسجيل الوصول عبر الإنترنت", "myBalances": "رصيدي", - "myWallet": "محف��تي", + "myWallet": "محفظتي", "balanceAmount": "مبلغ المحفظة", "totalBalance": "إجمالي الرصيد", "createAdvancedPayment": "إعادة شحن المحفظة", @@ -487,7 +489,7 @@ "services2": "الخدمات", "cantSeeProfile": "لرؤية ملفك الطبي، يرجى تسجيل الدخول أو التسجيل الآن", "loginRegisterNow": "تسجيل الدخول أو التسجيل الآن", - "hmgPharmacy": "صيدلية مجموعة الحبيب الطبية", + "hmgPharmacy": "صيدلية الحبيب", "ecommerceSolution": "حلول التجارة الإلكترونية", "comprehensive": "شامل", "onlineConsulting": "استشارات عبر الإنترنت", @@ -858,7 +860,7 @@ "onboardingBody1": "ببضع نقرات فقط يمكنك استشارة الطبيب الذي تختاره.", "onboardingHeading2": "الوصول إلى السجل الطبي بين يديك", "onboardingBody2": "تتبع تاريخك الطبي بما في ذلك الفحوصات المخبرية، الوصفات الطبية، التأمين، وغيرها.", - "hmgHospitals": "مستشفيات مجموعة الحبيب الطبية", + "hmgHospitals": "مستشفيات الحبيب", "hmcMedicalClinic": "مراكز مجموعة الحبيب الطبية", "applyFilter": "تطبيق الفلتر", "facilityAndLocation": "المرفق والموقع", @@ -908,6 +910,7 @@ "general": "عام", "liveCare": "لايف كير", "recentVisits": "الزيارات الأخيرة", + "favouriteDoctors": "الأطباء المفضلون", "searchByClinic": "البحث حسب العيادة", "tapToSelectClinic": "انقر لاختيار العيادة", "searchByDoctor": "البحث حسب الطبيب", @@ -1584,10 +1587,26 @@ "reschedulingAppo": "إعادة جدولة الموعد، يرجى الانتظار...", "invalidEligibility": "لا يمكنك إجراء الدفع عبر الإنترنت لأنك غير مؤهل لاستخدام الخدمة المقدمة.", "invalidInsurance": "لا يمكنك إجراء الدفع عبر الإنترنت لأنه ليس لديك تأمين صالح.", + "continueCash": "تواصل نقدا", + "applewatch": "ساعة آبل", + "applehealthapplicationshouldbeinstalledinyourphone": "يجب تثبيت تطبيق Apple Health على هاتفك", + "unabletodetectapplicationinstalledpleasecomebackonceinstalled": "لا يمكننا اكتشاف التطبيق المثبت على جهازك. يرجى العودة إلى هنا بمجرد تثبيت هذا التطبيق.", + "applewatchshouldbeconnected": "يجب توصيل ساعة آبل", + "samsungwatch": "ساعة سامسونج", + "samsunghealthapplicationshouldbeinstalledinyourphone": "يجب تثبيت تطبيق Samsung Health على هاتفك", + "samsungwatchshouldbeconnected": "يجب توصيل ساعة سامسونج", + "huaweiwatch": "ساعة هواوي", + "huaweihealthapplicationshouldbeinstalledinyourphone": "يجب تثبيت تطبيق Huawei Health على هاتفك", + "huaweiwatchshouldbeconnected": "يجب توصيل ساعة هواوي", + "whoopwatch": "ساعة Whoop", + "whoophealthapplicationshouldbeinstalledinyourphone": "يجب تثبيت تطبيق Whoop Health على هاتفك", + "whoopwatchshouldbeconnected": "يجب توصيل ساعة Whoop", + "updatetheinformation": "سيتيح ذلك جمع أحدث المعلومات من ساعة آبل الخاصة بك", "continueCash": "متابعة الدفع نقدًا", "timeFor": "الوقت", "hmgPolicies": "سياسات مجموعة الحبيب الطبية", "darkMode": "المظهر الداكن", + "featureComingSoonDescription": "هذه الميزة ستتوفر قريباً. نحن نعمل جاهدين لإضافة ميزات أكثر تميزاً إلى التطبيق. انتظرونا لمتابعة التحديثات.", "generateAiAnalysisResult": "قم بإجراء تحليل لهذا المختبر AI", "ratings": "التقييمات", "hmgPharmacyText": "صيدلية الحبيب، المتجر الصيدلاني الإلكتروني المتكامل الذي تقدمه لكم مجموعة خدمات الدكتور سليمان الحبيب الطبية.", @@ -1596,5 +1615,43 @@ "verifyInsurance": "التحقق من التأمين", "tests": "تحليل", "calendarPermissionAlert": "يرجى منح إذن الوصول إلى التقويم من إعدادات التطبيق لضبط تذكير تناول الدواء.", - "sortByLocation": "الترتيب حسب الموقع" + "sortByNearestLocation": "فرز حسب الأقرب إلى موقعك", + "giveLocationPermissionForNearestList": "يرجى منح إذن الوصول إلى الموقع من إعدادات التطبيق لعرض أقرب المواقع.", + "sortByLocation": "الترتيب حسب الموقع", + "timeForFirstReminder": "وقت التذكير الأول", + "reminderRemovalNote": "يمكنك إزالتها من التقويم الخاص بك لاحقاً عن طريق إيقاف تشغيل التذكير", + "communicationLanguage": "لغة التواصل", + + "cmcServiceHeader": "فحص صحي شامل: تشخيص متقدم، معلومات صحية مفصلة", + "cmcServiceDescription": "احصل على معلومات تفصيلية عن صحتك من خلال خدمات التشخيص المتقدمة لدينا. افهم جسمك بشكل أفضل لمستقبل صحي.", + "eReferralServiceHeader": "نظام الإحالة الإلكترونية في مستشفى حبيب: تبسيط عملية إحالة المرضى", + "eReferralServiceDescription": "نُسهّل عملية نقل المرضى بسلاسة إلى مستشفى حبيب من خلال نظام الإحالة الإلكترونية الآمن لدينا. نضمن استمرارية الرعاية لكل مريض.", + "bloodDonationServiceHeader": "تبرع بالدم، أنقذ الأرواح. تبرعك يُحدث فرقاً.", + "bloodDonationServiceDescription": "تبرّع بالدم، وأنقذ الأرواح. تبرعك يبعث الأمل. انضم لحملة التبرع بالدم وكن شريان حياة للمحتاجين. كل قطرة تُحدث فرقًا!", + "healthTrackersServiceHeader": "تتبّع مؤشراتك الحيوية بسهولة ويسر", + "healthTrackersServiceDescription": "أدخل بياناتك لمراقبة معدل ضربات القلب وضغط الدم بشكل مستمر، بالإضافة إلى ملخصات دقيقة لأنشطتك اليومية. ابقَ على اطلاع وحسّن صحتك بسهولة.", + "waterConsumptionServiceHeader": "حافظ على رطوبتك، حافظ على صحتك. تتبع كمية الماء التي تشربها يومياً بكل سهولة.", + "waterConsumptionServiceDescription": "أروِ عطشك، وتابع صحتك. راقب كمية الماء التي تتناولها يومياً بكل سهولة باستخدام تطبيقنا سهل الاستخدام، مما يضمن لك الترطيب الأمثل والصحة الجيدة.", + "smartWatchServiceHeader": "قم بمزامنة ساعتك الذكية مع تطبيقات الصحة", + "smartWatchServiceDescription": "قم بتوصيل ساعتك الذكية بسلاسة بتطبيقنا الصحي لتتبع البيانات بسهولة والحصول على رؤى شخصية.", + "liveChatServiceHeader": "مساعدة الخبراء على مدار الساعة طوال أيام الأسبوع\n\nمساعدة", + "liveChatServiceDescription": "هل تحتاج إلى مساعدة؟ تتيح لك خدمة الدردشة المباشرة لدينا التواصل مع فريق دعم الخبراء للإجابة على أي أسئلة لديك حول الميزات أو الإعدادات أو استكشاف الأخطاء وإصلاحها.", + "emergencyServiceHeader": "تسجيل الوصول إلى قسم الطوارئ، أسرع من أي وقت مضى. اتصل بالإسعاف / فريق الاستجابة السريعة على الفور", + "emergencyServiceDescription": "هل تواجه حالة طبية طارئة؟ سيارات الإسعاف وفرق الاستجابة السريعة لدينا جاهزة على مدار الساعة. بالإضافة إلى ذلك، يمكنك تسجيل دخولك إلى قسم الطوارئ بسرعة لتلقي رعاية أسرع.", + "homeHealthCareServiceHeader": "صحتك، في أبهى صورها. رعاية فائقة الجودة، تصلك إلى عتبة دارك.", + "homeHealthCareServiceDescription": "نقدم لكم رعاية صحية عالية الجودة تصلكم إلى عتبة منزلكم. ممرضات ذوات خبرة يقدمون رعاية حانية في راحة منزلكم.", + "profileOnlyText": "الملف الشخصي", + "information": "معلومة", + "noFavouriteDoctors": "ليس لديك أي قائمة مفضلة حتى الآن", + "addDoctors": "إضافة الأطباء", + "favouriteList": "قائمة المفضلة", + "later": "لاحقاً", + "cancelAppointmentConfirmMessage": "هل أنت متأكد من رغبتك في إلغاء هذا الموعد؟", + "acknowledged": "معترف به", + "searchLabResults": "بحث نتائج المختبر", + "callForAssistance": "اتصل للحصول على المساعدة الفورية", + "oneWaySubtitle": "نقل من الموقع إلى المستشفى", + "twoWaySubtitle": "نقل من الموقع إلى المستشفى والعودة مرة أخرى", + "toHospitalSubtitle": "نقل من موقعك الحالي إلى المستشفى", + "fromHospitalSubtitle": "نقل من المستشفى إلى منزلك" } diff --git a/assets/langs/en-US.json b/assets/langs/en-US.json index b0dd38ad..1bbd7cb7 100644 --- a/assets/langs/en-US.json +++ b/assets/langs/en-US.json @@ -3,6 +3,8 @@ "arabic": "Arabic", "login": "Login", "noDataAvailable": "No Data Available", + "noRatingAvailable": "No Rating Available", + "doctorDoesNotHaveRating": "This doctor does not have any ratings yet.", "confirm": "Confirm", "loadingText": "Loading, please wait...", "kilometerUnit": "KM", @@ -187,7 +189,7 @@ "lastName": "Last Name", "female": "Female", "male": "Male", - "preferredLanguage": "Preferred Language *", + "preferredLanguage": "Preferred Language", "locationsRegister": "Where do you want to create this file?", "ksa": "KSA", "dubai": "Dubai", @@ -300,7 +302,7 @@ "vitalSignsSubTitle": "Reports", "myMedical": "Active", "myMedicalSubtitle": "Medications", - "myDoctor": "My Doctor", + "myDoctor": "My Doctors", "myDoctorSubtitle": "List", "eye": "Eye", "eyeSubtitle": "Measurement", @@ -902,6 +904,7 @@ "general": "General", "liveCare": "LiveCare", "recentVisits": "Recent Visits", + "favouriteDoctors": "Favourite Doctors", "searchByClinic": "Search By Clinic", "tapToSelectClinic": "Tap to select clinic", "searchByDoctor": "Search By Doctor", @@ -1579,8 +1582,23 @@ "invalidEligibility": "You cannot make online payment because you are not eligible to use the provided service.", "invalidInsurance": "You cannot make online payment because you do not have a valid insurance.", "continueCash": "Continue as cash", + "applewatch": "Apple Watch", + "applehealthapplicationshouldbeinstalledinyourphone": "Apple Health application should be installed in your phone", + "unabletodetectapplicationinstalledpleasecomebackonceinstalled": "We are unable to detect the application installed in your device. Please come back here once you have installed this application.", + "applewatchshouldbeconnected": "Apple Watch should be connected", + "samsungwatch": "Samsung Watch", + "samsunghealthapplicationshouldbeinstalledinyourphone": "Samsung Health application should be installed in your phone", + "samsungwatchshouldbeconnected": "Samsung Watch should be connected", + "huaweiwatch": "Huawei Watch", + "huaweihealthapplicationshouldbeinstalledinyourphone": "Huawei Health application should be installed in your phone", + "huaweiwatchshouldbeconnected": "Huawei Watch should be connected", + "whoopwatch": "Whoop Watch", + "whoophealthapplicationshouldbeinstalledinyourphone": "Whoop Health application should be installed in your phone", + "whoopwatchshouldbeconnected": "Whoop Watch should be connected", + "updatetheinformation": "This will allow to gather the most up to date information from your apple watch", "timeFor": "Time For", "hmgPolicies": "HMG Policies", + "featureComingSoonDescription": "Feature is coming soon. We are actively working to bring more exciting features into the app. Stay tuned for updates.", "darkMode": "Dark Mode", "generateAiAnalysisResult": "Generate AI analysis for this result", "ratings": "Ratings", @@ -1590,5 +1608,42 @@ "verifyInsurance": "Verify Insurance", "tests": "tests", "calendarPermissionAlert": "Please grant calendar access permission from app settings to set medication reminder.", - "sortByLocation": "Sort by location" + "timeForFirstReminder": "Time for 1st reminder", + "reminderRemovalNote": "You can remove it from your calendar later by switching off the reminder", + "sortByLocation": "Sort by location", + "sortByNearestLocation": "Sort by nearest to your location", + "giveLocationPermissionForNearestList": "Please grant location permission from app settings to see the nearest locations.", + "communicationLanguage": "Communication Language", + "cmcServiceHeader": "Complete Health Checkup: Advanced diagnostics, Detailed Health insights", + "cmcServiceDescription": "Get detailed insights into your health with our advanced diagnostics. Understand your body better for a healthier future.", + "eReferralServiceHeader": "HMG Hospital E-Referral: Streamlined patient referrals", + "eReferralServiceDescription": "Facilitate seamless patient transfers to HMG with our secure e-referral system. Ensure continuity of care for every patient.", + "bloodDonationServiceHeader": "Give Blood, Save Lives. Your donation makes a difference.", + "bloodDonationServiceDescription": "Donate blood, empower lives. Your contribution creates hope. Join our blood drive and be a lifeline for those in need. Every drop counts!", + "healthTrackersServiceHeader": "Track Your Vitals with Ease and effortlessly ", + "healthTrackersServiceDescription": "Input your metrics for continuous heart rate monitoring, blood pressure and precise daily activity summaries. Stay informed and optimize your well-being with ease.", + "waterConsumptionServiceHeader": "Stay Hydrated, Stay Healthy. Track your daily water intake with ease.", + "waterConsumptionServiceDescription": "Quench your thirst, track your health. Effortlessly monitor your daily water intake with our user-friendly app, ensuring optimal hydration and well-being.", + "smartWatchServiceHeader": "Sync Your Smartwatch with Health Apps", + "smartWatchServiceDescription": "Seamlessly connect your smartwatch to our health app for effortless data tracking and personalized insights.", + "liveChatServiceHeader": "24/7 Expert\nAssistance", + "liveChatServiceDescription": "Need help ? Our live chat connects you with expert support for any questions about features, settings, or troubleshooting.", + "emergencyServiceHeader": "ER Check-in, Faster Than Ever. Call ambulance / Rapid Response Team instantly", + "emergencyServiceDescription": "In a medical emergency? Our ambulances and rapid response teams are on standby 24/7. Plus, quick ER check-in for faster care.", + "homeHealthCareServiceHeader": "Your Health, Elevated. Premium care, delivered to your doorstep.", + "homeHealthCareServiceDescription": "We bring quality healthcare to your doorstep. Experienced nurses providing compassionate care in the comfort of your home.", + "profileOnlyText": "Profile", + "information": "Information", + "noFavouriteDoctors": "You don't have any favourite list yet", + "addDoctors": "Add Doctors", + "favouriteList": "Favourite List", + "later": "Later", + "cancelAppointmentConfirmMessage": "Are you sure you want to cancel this appointment?", + "acknowledged": "Acknowledged", + "searchLabResults": "Search lab results", + "callForAssistance": "Call for immediate assistance", + "oneWaySubtitle": "Pickup from location to hospital", + "twoWaySubtitle": "Round trip from location to hospital and back", + "toHospitalSubtitle": "Transfer from your current location to the hospital", + "fromHospitalSubtitle": "Transfer from the hospital back to your home" } diff --git a/ios/Podfile.lock b/ios/Podfile.lock new file mode 100644 index 00000000..02c8b5da --- /dev/null +++ b/ios/Podfile.lock @@ -0,0 +1,543 @@ +PODS: + - amazon_payfort (1.1.4): + - Flutter + - PayFortSDK + - audio_session (0.0.1): + - Flutter + - barcode_scan2 (0.0.1): + - Flutter + - SwiftProtobuf (~> 1.33) + - connectivity_plus (0.0.1): + - Flutter + - CryptoSwift (1.8.4) + - device_calendar (0.0.1): + - Flutter + - device_calendar_plus_ios (0.0.1): + - Flutter + - device_info_plus (0.0.1): + - Flutter + - DKImagePickerController/Core (4.3.9): + - DKImagePickerController/ImageDataManager + - DKImagePickerController/Resource + - DKImagePickerController/ImageDataManager (4.3.9) + - DKImagePickerController/PhotoGallery (4.3.9): + - DKImagePickerController/Core + - DKPhotoGallery + - DKImagePickerController/Resource (4.3.9) + - DKPhotoGallery (0.0.19): + - DKPhotoGallery/Core (= 0.0.19) + - DKPhotoGallery/Model (= 0.0.19) + - DKPhotoGallery/Preview (= 0.0.19) + - DKPhotoGallery/Resource (= 0.0.19) + - SDWebImage + - SwiftyGif + - DKPhotoGallery/Core (0.0.19): + - DKPhotoGallery/Model + - DKPhotoGallery/Preview + - SDWebImage + - SwiftyGif + - DKPhotoGallery/Model (0.0.19): + - SDWebImage + - SwiftyGif + - DKPhotoGallery/Preview (0.0.19): + - DKPhotoGallery/Model + - DKPhotoGallery/Resource + - SDWebImage + - SwiftyGif + - DKPhotoGallery/Resource (0.0.19): + - SDWebImage + - SwiftyGif + - file_picker (0.0.1): + - DKImagePickerController/PhotoGallery + - Flutter + - Firebase/Analytics (11.15.0): + - Firebase/Core + - Firebase/Core (11.15.0): + - Firebase/CoreOnly + - FirebaseAnalytics (~> 11.15.0) + - Firebase/CoreOnly (11.15.0): + - FirebaseCore (~> 11.15.0) + - Firebase/Messaging (11.15.0): + - Firebase/CoreOnly + - FirebaseMessaging (~> 11.15.0) + - firebase_analytics (11.6.0): + - Firebase/Analytics (= 11.15.0) + - firebase_core + - Flutter + - firebase_core (3.15.2): + - Firebase/CoreOnly (= 11.15.0) + - Flutter + - firebase_messaging (15.2.10): + - Firebase/Messaging (= 11.15.0) + - firebase_core + - Flutter + - FirebaseAnalytics (11.15.0): + - FirebaseAnalytics/Default (= 11.15.0) + - FirebaseCore (~> 11.15.0) + - FirebaseInstallations (~> 11.0) + - GoogleUtilities/AppDelegateSwizzler (~> 8.1) + - GoogleUtilities/MethodSwizzler (~> 8.1) + - GoogleUtilities/Network (~> 8.1) + - "GoogleUtilities/NSData+zlib (~> 8.1)" + - nanopb (~> 3.30910.0) + - FirebaseAnalytics/Default (11.15.0): + - FirebaseCore (~> 11.15.0) + - FirebaseInstallations (~> 11.0) + - GoogleAppMeasurement/Default (= 11.15.0) + - GoogleUtilities/AppDelegateSwizzler (~> 8.1) + - GoogleUtilities/MethodSwizzler (~> 8.1) + - GoogleUtilities/Network (~> 8.1) + - "GoogleUtilities/NSData+zlib (~> 8.1)" + - nanopb (~> 3.30910.0) + - FirebaseCore (11.15.0): + - FirebaseCoreInternal (~> 11.15.0) + - GoogleUtilities/Environment (~> 8.1) + - GoogleUtilities/Logger (~> 8.1) + - FirebaseCoreInternal (11.15.0): + - "GoogleUtilities/NSData+zlib (~> 8.1)" + - FirebaseInstallations (11.15.0): + - FirebaseCore (~> 11.15.0) + - GoogleUtilities/Environment (~> 8.1) + - GoogleUtilities/UserDefaults (~> 8.1) + - PromisesObjC (~> 2.4) + - FirebaseMessaging (11.15.0): + - FirebaseCore (~> 11.15.0) + - FirebaseInstallations (~> 11.0) + - GoogleDataTransport (~> 10.0) + - GoogleUtilities/AppDelegateSwizzler (~> 8.1) + - GoogleUtilities/Environment (~> 8.1) + - GoogleUtilities/Reachability (~> 8.1) + - GoogleUtilities/UserDefaults (~> 8.1) + - nanopb (~> 3.30910.0) + - FLAnimatedImage (1.0.17) + - Flutter (1.0.0) + - flutter_callkit_incoming (0.0.1): + - CryptoSwift + - Flutter + - flutter_inappwebview_ios (0.0.1): + - Flutter + - flutter_inappwebview_ios/Core (= 0.0.1) + - OrderedSet (~> 6.0.3) + - flutter_inappwebview_ios/Core (0.0.1): + - Flutter + - OrderedSet (~> 6.0.3) + - flutter_ios_voip_kit_karmm (0.8.0): + - Flutter + - flutter_local_notifications (0.0.1): + - Flutter + - flutter_nfc_kit (3.6.0): + - Flutter + - flutter_zoom_videosdk (0.0.1): + - Flutter + - ZoomVideoSDK/CptShare (= 2.1.10) + - ZoomVideoSDK/zm_annoter_dynamic (= 2.1.10) + - ZoomVideoSDK/zoomcml (= 2.1.10) + - ZoomVideoSDK/ZoomVideoSDK (= 2.1.10) + - fluttertoast (0.0.2): + - Flutter + - geolocator_apple (1.2.0): + - Flutter + - FlutterMacOS + - Google-Maps-iOS-Utils (5.0.0): + - GoogleMaps (~> 8.0) + - google_maps_flutter_ios (0.0.1): + - Flutter + - Google-Maps-iOS-Utils (< 7.0, >= 5.0) + - GoogleMaps (< 11.0, >= 8.4) + - GoogleAdsOnDeviceConversion (2.1.0): + - GoogleUtilities/Logger (~> 8.1) + - GoogleUtilities/Network (~> 8.1) + - nanopb (~> 3.30910.0) + - GoogleAppMeasurement/Core (11.15.0): + - GoogleUtilities/AppDelegateSwizzler (~> 8.1) + - GoogleUtilities/MethodSwizzler (~> 8.1) + - GoogleUtilities/Network (~> 8.1) + - "GoogleUtilities/NSData+zlib (~> 8.1)" + - nanopb (~> 3.30910.0) + - GoogleAppMeasurement/Default (11.15.0): + - GoogleAdsOnDeviceConversion (= 2.1.0) + - GoogleAppMeasurement/Core (= 11.15.0) + - GoogleAppMeasurement/IdentitySupport (= 11.15.0) + - GoogleUtilities/AppDelegateSwizzler (~> 8.1) + - GoogleUtilities/MethodSwizzler (~> 8.1) + - GoogleUtilities/Network (~> 8.1) + - "GoogleUtilities/NSData+zlib (~> 8.1)" + - nanopb (~> 3.30910.0) + - GoogleAppMeasurement/IdentitySupport (11.15.0): + - GoogleAppMeasurement/Core (= 11.15.0) + - GoogleUtilities/AppDelegateSwizzler (~> 8.1) + - GoogleUtilities/MethodSwizzler (~> 8.1) + - GoogleUtilities/Network (~> 8.1) + - "GoogleUtilities/NSData+zlib (~> 8.1)" + - nanopb (~> 3.30910.0) + - GoogleDataTransport (10.1.0): + - nanopb (~> 3.30910.0) + - PromisesObjC (~> 2.4) + - GoogleMaps (8.4.0): + - GoogleMaps/Maps (= 8.4.0) + - GoogleMaps/Base (8.4.0) + - GoogleMaps/Maps (8.4.0): + - GoogleMaps/Base + - GoogleUtilities/AppDelegateSwizzler (8.1.0): + - GoogleUtilities/Environment + - GoogleUtilities/Logger + - GoogleUtilities/Network + - GoogleUtilities/Privacy + - GoogleUtilities/Environment (8.1.0): + - GoogleUtilities/Privacy + - GoogleUtilities/Logger (8.1.0): + - GoogleUtilities/Environment + - GoogleUtilities/Privacy + - GoogleUtilities/MethodSwizzler (8.1.0): + - GoogleUtilities/Logger + - GoogleUtilities/Privacy + - GoogleUtilities/Network (8.1.0): + - GoogleUtilities/Logger + - "GoogleUtilities/NSData+zlib" + - GoogleUtilities/Privacy + - GoogleUtilities/Reachability + - "GoogleUtilities/NSData+zlib (8.1.0)": + - GoogleUtilities/Privacy + - GoogleUtilities/Privacy (8.1.0) + - GoogleUtilities/Reachability (8.1.0): + - GoogleUtilities/Logger + - GoogleUtilities/Privacy + - GoogleUtilities/UserDefaults (8.1.0): + - GoogleUtilities/Logger + - GoogleUtilities/Privacy + - health (13.1.4): + - Flutter + - image_picker_ios (0.0.1): + - Flutter + - just_audio (0.0.1): + - Flutter + - FlutterMacOS + - local_auth_darwin (0.0.1): + - Flutter + - FlutterMacOS + - location (0.0.1): + - Flutter + - manage_calendar_events (0.0.1): + - Flutter + - map_launcher (0.0.1): + - Flutter + - MapboxCommon (23.11.0) + - MapboxCoreMaps (10.19.1): + - MapboxCommon (~> 23.11) + - MapboxCoreNavigation (2.19.0): + - MapboxDirections (~> 2.14) + - MapboxNavigationNative (< 207.0.0, >= 206.0.1) + - MapboxDirections (2.14.3): + - Polyline (~> 5.0) + - Turf (~> 2.8.0) + - MapboxMaps (10.19.0): + - MapboxCommon (= 23.11.0) + - MapboxCoreMaps (= 10.19.1) + - MapboxMobileEvents (= 2.0.0) + - Turf (= 2.8.0) + - MapboxMobileEvents (2.0.0) + - MapboxNavigation (2.19.0): + - MapboxCoreNavigation (= 2.19.0) + - MapboxMaps (~> 10.18) + - MapboxSpeech (~> 2.0) + - Solar-dev (~> 3.0) + - MapboxNavigationNative (206.2.2): + - MapboxCommon (~> 23.10) + - MapboxSpeech (2.1.1) + - nanopb (3.30910.0): + - nanopb/decode (= 3.30910.0) + - nanopb/encode (= 3.30910.0) + - nanopb/decode (3.30910.0) + - nanopb/encode (3.30910.0) + - network_info_plus (0.0.1): + - Flutter + - open_filex (0.0.2): + - Flutter + - OrderedSet (6.0.3) + - package_info_plus (0.4.5): + - Flutter + - path_provider_foundation (0.0.1): + - Flutter + - FlutterMacOS + - PayFortSDK (3.2.1) + - permission_handler_apple (9.3.0): + - Flutter + - Polyline (5.1.0) + - PromisesObjC (2.4.0) + - SDWebImage (5.21.5): + - SDWebImage/Core (= 5.21.5) + - SDWebImage/Core (5.21.5) + - share_plus (0.0.1): + - Flutter + - shared_preferences_foundation (0.0.1): + - Flutter + - FlutterMacOS + - Solar-dev (3.0.1) + - sqflite_darwin (0.0.4): + - Flutter + - FlutterMacOS + - SwiftProtobuf (1.33.3) + - SwiftyGif (5.4.5) + - Turf (2.8.0) + - url_launcher_ios (0.0.1): + - Flutter + - video_player_avfoundation (0.0.1): + - Flutter + - FlutterMacOS + - wakelock_plus (0.0.1): + - Flutter + - webview_flutter_wkwebview (0.0.1): + - Flutter + - FlutterMacOS + - ZoomVideoSDK/CptShare (2.1.10) + - ZoomVideoSDK/zm_annoter_dynamic (2.1.10) + - ZoomVideoSDK/zoomcml (2.1.10) + - ZoomVideoSDK/ZoomVideoSDK (2.1.10) + +DEPENDENCIES: + - amazon_payfort (from `.symlinks/plugins/amazon_payfort/ios`) + - audio_session (from `.symlinks/plugins/audio_session/ios`) + - barcode_scan2 (from `.symlinks/plugins/barcode_scan2/ios`) + - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) + - device_calendar (from `.symlinks/plugins/device_calendar/ios`) + - device_calendar_plus_ios (from `.symlinks/plugins/device_calendar_plus_ios/ios`) + - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) + - file_picker (from `.symlinks/plugins/file_picker/ios`) + - firebase_analytics (from `.symlinks/plugins/firebase_analytics/ios`) + - firebase_core (from `.symlinks/plugins/firebase_core/ios`) + - firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`) + - FLAnimatedImage + - Flutter (from `Flutter`) + - flutter_callkit_incoming (from `.symlinks/plugins/flutter_callkit_incoming/ios`) + - flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`) + - flutter_ios_voip_kit_karmm (from `.symlinks/plugins/flutter_ios_voip_kit_karmm/ios`) + - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`) + - flutter_nfc_kit (from `.symlinks/plugins/flutter_nfc_kit/ios`) + - flutter_zoom_videosdk (from `.symlinks/plugins/flutter_zoom_videosdk/ios`) + - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`) + - geolocator_apple (from `.symlinks/plugins/geolocator_apple/darwin`) + - google_maps_flutter_ios (from `.symlinks/plugins/google_maps_flutter_ios/ios`) + - health (from `.symlinks/plugins/health/ios`) + - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) + - just_audio (from `.symlinks/plugins/just_audio/darwin`) + - local_auth_darwin (from `.symlinks/plugins/local_auth_darwin/darwin`) + - location (from `.symlinks/plugins/location/ios`) + - manage_calendar_events (from `.symlinks/plugins/manage_calendar_events/ios`) + - map_launcher (from `.symlinks/plugins/map_launcher/ios`) + - MapboxMaps (= 10.19.0) + - MapboxNavigation (= 2.19.0) + - network_info_plus (from `.symlinks/plugins/network_info_plus/ios`) + - open_filex (from `.symlinks/plugins/open_filex/ios`) + - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) + - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) + - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) + - share_plus (from `.symlinks/plugins/share_plus/ios`) + - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) + - sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`) + - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) + - video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`) + - wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`) + - webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/darwin`) + +SPEC REPOS: + trunk: + - CryptoSwift + - DKImagePickerController + - DKPhotoGallery + - Firebase + - FirebaseAnalytics + - FirebaseCore + - FirebaseCoreInternal + - FirebaseInstallations + - FirebaseMessaging + - FLAnimatedImage + - Google-Maps-iOS-Utils + - GoogleAdsOnDeviceConversion + - GoogleAppMeasurement + - GoogleDataTransport + - GoogleMaps + - GoogleUtilities + - MapboxCommon + - MapboxCoreMaps + - MapboxCoreNavigation + - MapboxDirections + - MapboxMaps + - MapboxMobileEvents + - MapboxNavigation + - MapboxNavigationNative + - MapboxSpeech + - nanopb + - OrderedSet + - PayFortSDK + - Polyline + - PromisesObjC + - SDWebImage + - Solar-dev + - SwiftProtobuf + - SwiftyGif + - Turf + - ZoomVideoSDK + +EXTERNAL SOURCES: + amazon_payfort: + :path: ".symlinks/plugins/amazon_payfort/ios" + audio_session: + :path: ".symlinks/plugins/audio_session/ios" + barcode_scan2: + :path: ".symlinks/plugins/barcode_scan2/ios" + connectivity_plus: + :path: ".symlinks/plugins/connectivity_plus/ios" + device_calendar: + :path: ".symlinks/plugins/device_calendar/ios" + device_calendar_plus_ios: + :path: ".symlinks/plugins/device_calendar_plus_ios/ios" + device_info_plus: + :path: ".symlinks/plugins/device_info_plus/ios" + file_picker: + :path: ".symlinks/plugins/file_picker/ios" + firebase_analytics: + :path: ".symlinks/plugins/firebase_analytics/ios" + firebase_core: + :path: ".symlinks/plugins/firebase_core/ios" + firebase_messaging: + :path: ".symlinks/plugins/firebase_messaging/ios" + Flutter: + :path: Flutter + flutter_callkit_incoming: + :path: ".symlinks/plugins/flutter_callkit_incoming/ios" + flutter_inappwebview_ios: + :path: ".symlinks/plugins/flutter_inappwebview_ios/ios" + flutter_ios_voip_kit_karmm: + :path: ".symlinks/plugins/flutter_ios_voip_kit_karmm/ios" + flutter_local_notifications: + :path: ".symlinks/plugins/flutter_local_notifications/ios" + flutter_nfc_kit: + :path: ".symlinks/plugins/flutter_nfc_kit/ios" + flutter_zoom_videosdk: + :path: ".symlinks/plugins/flutter_zoom_videosdk/ios" + fluttertoast: + :path: ".symlinks/plugins/fluttertoast/ios" + geolocator_apple: + :path: ".symlinks/plugins/geolocator_apple/darwin" + google_maps_flutter_ios: + :path: ".symlinks/plugins/google_maps_flutter_ios/ios" + health: + :path: ".symlinks/plugins/health/ios" + image_picker_ios: + :path: ".symlinks/plugins/image_picker_ios/ios" + just_audio: + :path: ".symlinks/plugins/just_audio/darwin" + local_auth_darwin: + :path: ".symlinks/plugins/local_auth_darwin/darwin" + location: + :path: ".symlinks/plugins/location/ios" + manage_calendar_events: + :path: ".symlinks/plugins/manage_calendar_events/ios" + map_launcher: + :path: ".symlinks/plugins/map_launcher/ios" + network_info_plus: + :path: ".symlinks/plugins/network_info_plus/ios" + open_filex: + :path: ".symlinks/plugins/open_filex/ios" + package_info_plus: + :path: ".symlinks/plugins/package_info_plus/ios" + path_provider_foundation: + :path: ".symlinks/plugins/path_provider_foundation/darwin" + permission_handler_apple: + :path: ".symlinks/plugins/permission_handler_apple/ios" + share_plus: + :path: ".symlinks/plugins/share_plus/ios" + shared_preferences_foundation: + :path: ".symlinks/plugins/shared_preferences_foundation/darwin" + sqflite_darwin: + :path: ".symlinks/plugins/sqflite_darwin/darwin" + url_launcher_ios: + :path: ".symlinks/plugins/url_launcher_ios/ios" + video_player_avfoundation: + :path: ".symlinks/plugins/video_player_avfoundation/darwin" + wakelock_plus: + :path: ".symlinks/plugins/wakelock_plus/ios" + webview_flutter_wkwebview: + :path: ".symlinks/plugins/webview_flutter_wkwebview/darwin" + +SPEC CHECKSUMS: + amazon_payfort: 4ad7a3413acc1c4c4022117a80d18fee23c572d3 + audio_session: 9bb7f6c970f21241b19f5a3658097ae459681ba0 + barcode_scan2: 4e4b850b112f4e29017833e4715f36161f987966 + connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd + CryptoSwift: e64e11850ede528a02a0f3e768cec8e9d92ecb90 + device_calendar: b55b2c5406cfba45c95a59f9059156daee1f74ed + device_calendar_plus_ios: 2c04ad7643c6e697438216e33693b84e8ca45ded + device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe + DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c + DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 + file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be + Firebase: d99ac19b909cd2c548339c2241ecd0d1599ab02e + firebase_analytics: 0e25ca1d4001ccedd40b4e5b74c0ec34e18f6425 + firebase_core: 995454a784ff288be5689b796deb9e9fa3601818 + firebase_messaging: f4a41dd102ac18b840eba3f39d67e77922d3f707 + FirebaseAnalytics: 6433dfd311ba78084fc93bdfc145e8cb75740eae + FirebaseCore: efb3893e5b94f32b86e331e3bd6dadf18b66568e + FirebaseCoreInternal: 9afa45b1159304c963da48addb78275ef701c6b4 + FirebaseInstallations: 317270fec08a5d418fdbc8429282238cab3ac843 + FirebaseMessaging: 3b26e2cee503815e01c3701236b020aa9b576f09 + FLAnimatedImage: bbf914596368867157cc71b38a8ec834b3eeb32b + Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 + flutter_callkit_incoming: cb8138af67cda6dd981f7101a5d709003af21502 + flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99 + flutter_ios_voip_kit_karmm: 371663476722afb631d5a13a39dee74c56c1abd0 + flutter_local_notifications: a5a732f069baa862e728d839dd2ebb904737effb + flutter_nfc_kit: e1b71583eafd2c9650bc86844a7f2d185fb414f6 + flutter_zoom_videosdk: 0f59e71685a03ddb0783ecc43bf3155b8599a7f5 + fluttertoast: 2c67e14dce98bbdb200df9e1acf610d7a6264ea1 + geolocator_apple: ab36aa0e8b7d7a2d7639b3b4e48308394e8cef5e + Google-Maps-iOS-Utils: 66d6de12be1ce6d3742a54661e7a79cb317a9321 + google_maps_flutter_ios: 3213e1e5f5588b6134935cb8fc59acb4e6d88377 + GoogleAdsOnDeviceConversion: 2be6297a4f048459e0ae17fad9bfd2844e10cf64 + GoogleAppMeasurement: 700dce7541804bec33db590a5c496b663fbe2539 + GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 + GoogleMaps: 8939898920281c649150e0af74aa291c60f2e77d + GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1 + health: 32d2fbc7f26f9a2388d1a514ce168adbfa5bda65 + image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326 + just_audio: 4e391f57b79cad2b0674030a00453ca5ce817eed + local_auth_darwin: c3ee6cce0a8d56be34c8ccb66ba31f7f180aaebb + location: 155caecf9da4f280ab5fe4a55f94ceccfab838f8 + manage_calendar_events: fe1541069431af035ced925ebd9def8b4b271254 + map_launcher: 8051ad5783913cafce93f2414c6858f2904fd8df + MapboxCommon: 119f3759f7dc9457f0695848108ab323eb643cb4 + MapboxCoreMaps: ca17f67baced23f8c952166ac6314c35bad3f66c + MapboxCoreNavigation: 3be9990fae3ed732a101001746d0e3b4234ec023 + MapboxDirections: d9ad8452e8927d95ed21e35f733834dbca7e0eb1 + MapboxMaps: b7f29ec7c33f7dc6d2947c1148edce6db81db9a7 + MapboxMobileEvents: d044b9edbe0ec7df60f6c2c9634fe9a7f449266b + MapboxNavigation: da9cf3d773ed5b0fa0fb388fccdaa117ee681f31 + MapboxNavigationNative: 629e359f3d2590acd1ebbacaaf99e1a80ee57e42 + MapboxSpeech: cd25ef99c3a3d2e0da72620ff558276ea5991a77 + nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 + network_info_plus: cf61925ab5205dce05a4f0895989afdb6aade5fc + open_filex: 432f3cd11432da3e39f47fcc0df2b1603854eff1 + OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94 + package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 + path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880 + PayFortSDK: 233eabe9a45601fdbeac67fa6e5aae46ed8faf82 + permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d + Polyline: 2a1f29f87f8d9b7de868940f4f76deb8c678a5b1 + PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 + SDWebImage: e9c98383c7572d713c1a0d7dd2783b10599b9838 + share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a + shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb + Solar-dev: 4612dc9878b9fed2667d23b327f1d4e54e16e8d0 + sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 + SwiftProtobuf: e1b437c8e31a4c5577b643249a0bb62ed4f02153 + SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 + Turf: aa2ede4298009639d10db36aba1a7ebaad072a5e + url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b + video_player_avfoundation: dd410b52df6d2466a42d28550e33e4146928280a + wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556 + webview_flutter_wkwebview: 8ebf4fded22593026f7dbff1fbff31ea98573c8d + ZoomVideoSDK: 94e939820e57a075c5e712559f927017da0de06a + +PODFILE CHECKSUM: 8235407385ddd5904afc2563d65406117a51993e + +COCOAPODS: 1.16.2 diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index 64d7428e..308891a7 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -16,11 +16,11 @@ import GoogleMaps return super.application(application, didFinishLaunchingWithOptions: launchOptions) } func initializePlatformChannels(){ - if let mainViewController = window?.rootViewController as? FlutterViewController{ // platform initialization suppose to be in foreground - - HMGPenguinInPlatformBridge.initialize(flutterViewController: mainViewController) - - } +// if let mainViewController = window?.rootViewController as? FlutterViewController{ // platform initialization suppose to be in foreground +// +//// HMGPenguinInPlatformBridge.initialize(flutterViewController: mainViewController) +// +// } } override func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken:Data){ // Messaging.messaging().apnsToken = deviceToken diff --git a/lib/core/api/api_client.dart b/lib/core/api/api_client.dart index 2de4ce5c..374ac9d1 100644 --- a/lib/core/api/api_client.dart +++ b/lib/core/api/api_client.dart @@ -200,7 +200,7 @@ class ApiClientImp implements ApiClient { } // body['TokenID'] = "@dm!n"; - // body['PatientID'] = 1231755; + // body['PatientID'] = 1018977; // body['PatientTypeID'] = 1; // body['PatientOutSA'] = 0; // body['SessionID'] = "45786230487560q"; diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart index 5fae22b1..5fb73a37 100644 --- a/lib/core/api_consts.dart +++ b/lib/core/api_consts.dart @@ -229,11 +229,12 @@ class ApiConsts { static String getPatientBloodGroup = "services/PatientVarification.svc/REST/BloodDonation_GetBloodGroupDetails"; static String getPatientBloodAgreement = "Services/PatientVarification.svc/REST/CheckUserAgreementForBloodDonation"; + static String getPatientBloodTypeNew = "Services/Patients.svc/REST/HIS_GetPatientBloodType_New"; static String getAiOverViewLabOrders = "Services/Patients.svc/REST/HMGAI_Lab_Analyze_Orders_API"; static String getAiOverViewLabOrder = "Services/Patients.svc/REST/HMGAI_Lab_Analyzer_API"; // ************ static values for Api **************** - static final double appVersionID = 20.5; + static final double appVersionID = 20.9; // static final double appVersionID = 50.7; static final int appChannelId = 3; @@ -468,6 +469,15 @@ var GET_DENTAL_DOCTORS_LIST_URL = "Services/Doctors.svc/REST/Dental_DoctorChiefC //URL to get doctor free slots var GET_DOCTOR_FREE_SLOTS = "Services/Doctors.svc/REST/GetDoctorFreeSlots"; +//URL to check if doctor is favorite +var IS_FAVOURITE_DOCTOR = "Services/Patients.svc/REST/Patient_IsFavouriteDoctor"; + +//URL to get favorite doctors list +var GET_FAVOURITE_DOCTOR = "Services/Patients.svc/REST/Patient_GetFavouriteDoctor"; + +//URL to insert favorite doctor +var INSERT_FAVOURITE_DOCTOR = "Services/Patients.svc/REST/Patient_InsertFavouriteDoctor"; + //URL to insert appointment var INSERT_SPECIFIC_APPOINTMENT = "Services/Doctors.svc/REST/InsertSpecificAppointment"; diff --git a/lib/core/app_assets.dart b/lib/core/app_assets.dart index 86fbb07e..d90cd40c 100644 --- a/lib/core/app_assets.dart +++ b/lib/core/app_assets.dart @@ -174,7 +174,7 @@ class AppAssets { static const String warning = '$svgBasePath/warning.svg'; static const String share_location = '$svgBasePath/share_location.svg'; static const String to_arrow = '$svgBasePath/to_arrow.svg'; - static const String dual_arrow = '$svgBasePath/to_arrow.svg'; + static const String dual_arrow = '$svgBasePath/dual_arrow.svg'; static const String forward_arrow_medium = '$svgBasePath/forward_arrow_medium.svg'; static const String eReferral = '$svgBasePath/e-referral.svg'; static const String comprehensiveCheckup = '$svgBasePath/comprehensive_checkup.svg'; @@ -182,6 +182,8 @@ class AppAssets { static const String ic_rrt_vehicle = '$svgBasePath/ic_rrt_vehicle.svg'; static const String doctor_profile_rating_icon = '$svgBasePath/doctor_profile_rating_icon.svg'; static const String doctor_profile_reviews_icon = '$svgBasePath/doctor_profile_reviews_icon.svg'; + static const String bookmark_icon = '$svgBasePath/bookmark_icon.svg'; + static const String bookmark_filled_icon = '$svgBasePath/bookmark_filled_icon.svg'; static const String waiting_appointment_icon = '$svgBasePath/waitingAppo.svg'; static const String call_for_vitals = '$svgBasePath/call_for_vitals.svg'; static const String call_for_doctor = '$svgBasePath/call_for_doctor.svg'; @@ -233,6 +235,21 @@ class AppAssets { static const String forward_top_nav_icon = '$svgBasePath/forward_top_nav_icon.svg'; static const String back_top_nav_icon = '$svgBasePath/back_top_nav_icon.svg'; + static const String bluetooth = '$svgBasePath/bluetooth.svg'; + + //smartwatch + static const String watchActivity = '$svgBasePath/watch_activity.svg'; + static const String watchActivityTrailing = '$svgBasePath/watch_activity_trailing.svg'; + static const String watchSteps= '$svgBasePath/watch_steps.svg'; + static const String watchStepsTrailing= '$svgBasePath/watch_steps_trailing.svg'; + static const String watchSleep= '$svgBasePath/watch_sleep.svg'; + static const String watchSleepTrailing= '$svgBasePath/watch_sleep_trailing.svg'; + static const String watchBmi= '$svgBasePath/watch_bmi.svg'; + static const String watchBmiTrailing= '$svgBasePath/watch_bmi_trailing.svg'; + static const String watchWeight= '$svgBasePath/watch_weight.svg'; + static const String watchWeightTrailing= '$svgBasePath/watch_weight_trailing.svg'; + static const String watchHeight= '$svgBasePath/watch_height.svg'; + //bottom navigation// static const String homeBottom = '$svgBasePath/home_bottom.svg'; @@ -344,6 +361,15 @@ class AppAssets { static const String homeHealthCareService = '$pngBasePath/home_health_care.png'; static const String pharmacyService = '$pngBasePath/pharmacy_service.png'; + static const String bloodDonationService = '$pngBasePath/blood_donation_image.png'; + static const String waterConsumptionService = '$pngBasePath/water_consumption_image.png'; + static const String emergencyService = '$pngBasePath/emergency_services_image.png'; + static const String cmcService = '$pngBasePath/cmc_services_image.png'; + static const String eReferralService = '$pngBasePath/ereferral_services_image.png'; + static const String carParkingService = '$pngBasePath/Carparking_services_image.png'; + static const String smartWatchService = '$pngBasePath/smartwatch_services_image.png'; + static const String healthTrackersService = '$pngBasePath/healthtrackers_services_image.png'; + static const String livechatService = '$pngBasePath/livechat_services_image.png'; static const String maleImg = '$pngBasePath/male_img.png'; static const String femaleImg = '$pngBasePath/female_img.png'; diff --git a/lib/core/app_state.dart b/lib/core/app_state.dart index 274d37f4..babc7b45 100644 --- a/lib/core/app_state.dart +++ b/lib/core/app_state.dart @@ -169,7 +169,7 @@ class AppState { ///this will be called if there is any problem in getting the user location void resetLocation() { userLong = 0.0; - userLong = 0.0; + userLat = 0.0; } setRatedVisible(bool value) { diff --git a/lib/core/common_models/smart_watch.dart b/lib/core/common_models/smart_watch.dart new file mode 100644 index 00000000..b9259f5a --- /dev/null +++ b/lib/core/common_models/smart_watch.dart @@ -0,0 +1,18 @@ +enum SmartWatchTypes{ + apple, + samsung, + huawei, + whoop +} + + +class SmartwatchDetails { + final SmartWatchTypes watchType; + final String watchIcon; + final String smallIcon; + final String detailsTitle; + final String details; + final String secondTitle; + + SmartwatchDetails(this.watchType, this.watchIcon, this.smallIcon, this.detailsTitle, this.details, this.secondTitle); +} \ No newline at end of file diff --git a/lib/core/utils/calender_utils_new.dart b/lib/core/utils/calender_utils_new.dart index 331cf6d5..6f043e26 100644 --- a/lib/core/utils/calender_utils_new.dart +++ b/lib/core/utils/calender_utils_new.dart @@ -101,9 +101,11 @@ class CalenderUtilsNew { required String itemDescriptionN, required String route, Function(String)? onFailure, - String? prescriptionNumber}) async { + String? prescriptionNumber, + DateTime? scheduleDateTime, + }) async { DateTime currentDay = DateTime.now(); - DateTime actualDate = DateTime(DateTime.now().year, DateTime.now().month, DateTime.now().day, 8, 0); + DateTime actualDate = scheduleDateTime??DateTime(DateTime.now().year, DateTime.now().month, DateTime.now().day, 8, 0); print("the frequency is $frequencyNumber"); frequencyNumber ??= 2; //Some time frequency number is null so by default will be 2 int interval = calculateIntervalAsPerFrequency(frequencyNumber); diff --git a/lib/core/utils/date_util.dart b/lib/core/utils/date_util.dart index 87af0a27..6c4e7bec 100644 --- a/lib/core/utils/date_util.dart +++ b/lib/core/utils/date_util.dart @@ -1,7 +1,10 @@ import 'package:device_calendar/device_calendar.dart'; import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:intl/intl.dart'; +import '../app_state.dart' show AppState; + class DateUtil { /// convert String To Date function /// [date] String we want to convert @@ -198,6 +201,10 @@ class DateUtil { } } + static getMonthDayAsOfLang(int month){ + return getIt.get().isArabic()?getMonthArabic(month):getMonth(month); + } + /// get month by /// [month] convert month number in to month name in Arabic static getMonthArabic(int month) { @@ -268,6 +275,10 @@ class DateUtil { return date ?? DateTime.now(); } + static getWeekDayAsOfLang(int weekDay){ + return getIt.get().isArabic()?getWeekDayArabic(weekDay):getWeekDayEnglish(weekDay); + } + /// get month by /// [weekDay] convert week day in int to week day name static getWeekDay(int weekDay) { @@ -580,6 +591,14 @@ class DateUtil { return weekDayName; // Return as-is if not recognized } } + + static String millisToHourMin(int milliseconds) { + int totalMinutes = (milliseconds / 60000).floor(); // convert ms → min + int hours = totalMinutes ~/ 60; // integer division + int minutes = totalMinutes % 60; // remaining minutes + + return '${hours} hr ${minutes} min'; + } } extension OnlyDate on DateTime { diff --git a/lib/core/utils/loading_utils.dart b/lib/core/utils/loading_utils.dart index bd24d30c..48d4f2dd 100644 --- a/lib/core/utils/loading_utils.dart +++ b/lib/core/utils/loading_utils.dart @@ -15,37 +15,37 @@ class LoadingUtils { static bool get isLoading => _isLoadingVisible; - static showFullScreenLoader({bool barrierDismissible = true, isSuccessDialog = false, String loadingText = "Loading, Please wait..."}) { - if (!_isLoadingVisible) { - _isLoadingVisible = true; - final context = _navigationService.navigatorKey.currentContext; - log("got the context in showFullScreenLoading"); - if (context == null) return; - - showDialog( - barrierDismissible: barrierDismissible, - context: context, - barrierColor: AppColors.blackColor.withOpacity(0.5), - useRootNavigator: false, - useSafeArea: false, - builder: (BuildContext context) { - return Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 20.h, - hasShadow: false, - ), - child: Material( - child: Center( - child: isSuccessDialog ? Utils.getSuccessWidget(loadingText: loadingText) : Utils.getLoadingWidget(loadingText: loadingText), - ).paddingSymmetrical(24.w, 0), - ), - ); - }).then((value) { - _isLoadingVisible = false; - }); - } - } + // static showFullScreenLoader({bool barrierDismissible = true, isSuccessDialog = false, String loadingText = "Loading, Please wait..."}) { + // if (!_isLoadingVisible) { + // _isLoadingVisible = true; + // final context = _navigationService.navigatorKey.currentContext; + // log("got the context in showFullScreenLoading"); + // if (context == null) return; + // + // showDialog( + // barrierDismissible: barrierDismissible, + // context: context, + // barrierColor: AppColors.blackColor.withOpacity(0.5), + // useRootNavigator: false, + // useSafeArea: false, + // builder: (BuildContext context) { + // return Container( + // decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + // color: AppColors.whiteColor, + // borderRadius: 20.h, + // hasShadow: false, + // ), + // child: Material( + // child: Center( + // child: isSuccessDialog ? Utils.getSuccessWidget(loadingText: loadingText) : Utils.getLoadingWidget(loadingText: loadingText), + // ).paddingSymmetrical(24.w, 0), + // ), + // ); + // }).then((value) { + // _isLoadingVisible = false; + // }); + // } + // } static hideFullScreenLoader() { if (!_isLoadingVisible) return; diff --git a/lib/core/utils/utils.dart b/lib/core/utils/utils.dart index 0d600bd7..18a8d6c8 100644 --- a/lib/core/utils/utils.dart +++ b/lib/core/utils/utils.dart @@ -152,9 +152,11 @@ class Utils { static String getDayMonthYearDateFormatted(DateTime? dateTime) { if (dateTime == null) return ""; - return appState.isArabic() - ? "${dateTime.day.toString()} ${getMonthArabic(dateTime.month)}, ${dateTime.year.toString()}" - : "${dateTime.day.toString()} ${getMonth(dateTime.month)}, ${dateTime.year.toString()}"; + return + // appState.isArabic() + // ? "${dateTime.day.toString()} ${getMonthArabic(dateTime.month)}, ${dateTime.year.toString()}" + // : + "${dateTime.day.toString()} ${getMonth(dateTime.month)}, ${dateTime.year.toString()}"; } /// get month by @@ -382,7 +384,7 @@ class Utils { children: [ Lottie.asset(AppAnimations.checkmark, repeat: true, reverse: false, frameRate: FrameRate(60), width: 100.h, height: 100.h, fit: BoxFit.fill), SizedBox(height: 8.h), - (loadingText ?? LocaleKeys.loadingText.tr()).toText16(color: AppColors.blackColor), + (loadingText ?? LocaleKeys.loadingText.tr()).toText16(color: AppColors.blackColor).paddingSymmetrical(24.h, 0.h), SizedBox(height: 8.h), ], ).center; @@ -396,7 +398,7 @@ class Utils { Lottie.asset(AppAnimations.errorAnimation, repeat: true, reverse: false, frameRate: FrameRate(60), width: 100.h, height: 100.h, fit: BoxFit.fill), SizedBox(height: 8.h), - (loadingText ?? LocaleKeys.loadingText.tr()).toText16(color: AppColors.blackColor), + (loadingText ?? LocaleKeys.loadingText.tr()).toText16(color: AppColors.blackColor).paddingSymmetrical(24.h, 0.h), SizedBox(height: 8.h), ], ).center; @@ -405,6 +407,7 @@ class Utils { static Widget getWarningWidget({ String? loadingText, bool isShowActionButtons = false, + bool showOkButton = false, Widget? bodyWidget, Function? onConfirmTap, Function? onCancelTap, @@ -457,7 +460,26 @@ class Utils { ), ], ) - : SizedBox.shrink(), + : showOkButton? + Row( + children: [ + Expanded( + child: CustomButton( + text: LocaleKeys.ok.tr(), + onPressed: () async { + if (onConfirmTap != null) { + onConfirmTap(); + } + }, + backgroundColor: AppColors.bgGreenColor, + borderColor: AppColors.bgGreenColor, + textColor: Colors.white, + // icon: AppAssets.confirm, + ), + ), + ], + ) + :SizedBox.shrink(), ], ).center; } @@ -748,21 +770,22 @@ class Utils { return Row( mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.spaceBetween, + spacing: 5.w, children: [ - Image.asset(AppAssets.mada, width: 25.h, height: 25.h), + Image.asset(AppAssets.mada, width: 35.h, height: 35.h), Image.asset( AppAssets.tamaraEng, - width: 25.h, - height: 25.h, + width: 35.h, + height: 35.h, fit: BoxFit.contain, errorBuilder: (context, error, stackTrace) { debugPrint('Failed to load Tamara PNG in payment methods: $error'); - return Utils.buildSvgWithAssets(icon: AppAssets.tamara, width: 25.h, height: 25.h, fit: BoxFit.contain); + return Utils.buildSvgWithAssets(icon: AppAssets.tamara, width: 35.h, height: 35.h, fit: BoxFit.contain); }, ), - Image.asset(AppAssets.visa, width: 25.h, height: 25.h), - Image.asset(AppAssets.mastercard, width: 25.h, height: 25.h), - Image.asset(AppAssets.applePay, width: 25.h, height: 25.h), + Image.asset(AppAssets.visa, width: 35.h, height: 35.h), + Image.asset(AppAssets.mastercard, width: 35.h, height: 25.h), + Image.asset(AppAssets.applePay, width: 35.h, height: 35.h), ], ); } diff --git a/lib/extensions/string_extensions.dart b/lib/extensions/string_extensions.dart index 6e28ffda..a68bdcf1 100644 --- a/lib/extensions/string_extensions.dart +++ b/lib/extensions/string_extensions.dart @@ -18,6 +18,7 @@ extension CapExtension on String { String get needTranslation => this; String get capitalizeFirstofEach => trim().isNotEmpty ? trim().toLowerCase().split(" ").map((str) => str.inCaps).join(" ") : ""; + } extension EmailValidator on String { @@ -72,13 +73,14 @@ extension EmailValidator on String { int? maxlines, FontStyle? fontStyle, TextOverflow? textOverflow, - double letterSpacing = 0}) => + double letterSpacing = 0, bool isEnglishOnly = false}) => Text( this, textAlign: isCenter ? TextAlign.center : null, maxLines: maxlines, overflow: textOverflow, style: TextStyle( + fontFamily: isEnglishOnly ? "Poppins" : getIt.get().getLanguageCode() == "ar" ? 'GESSTwo' : 'Poppins', fontSize: 9.f, fontStyle: fontStyle ?? FontStyle.normal, fontWeight: weight ?? (isBold ? FontWeight.bold : FontWeight.normal), @@ -88,7 +90,7 @@ extension EmailValidator on String { decorationColor: color ?? AppColors.blackColor), ); - Widget toText11({Color? color, FontWeight? weight, bool isUnderLine = false, bool isCenter = false, bool isBold = false, int maxLine = 0, double letterSpacing = 0}) => Text( + Widget toText11({Color? color, FontWeight? weight, bool isUnderLine = false, bool isCenter = false, bool isBold = false, int maxLine = 0, double letterSpacing = 0, bool isEnglishOnly = false,}) => Text( this, textAlign: isCenter ? TextAlign.center : null, maxLines: (maxLine > 0) ? maxLine : null, @@ -99,6 +101,7 @@ extension EmailValidator on String { color: color ?? AppColors.blackColor, letterSpacing: letterSpacing, decoration: isUnderLine ? TextDecoration.underline : null, + fontFamily: isEnglishOnly ? "Poppins" : getIt.get().getLanguageCode() == "ar" ? 'GESSTwo' : 'Poppins', ), ); @@ -293,17 +296,19 @@ extension EmailValidator on String { style: TextStyle(height: 1, color: color ?? AppColors.blackColor, fontSize: 22.f, letterSpacing: -1, fontWeight: isBold ? FontWeight.bold : FontWeight.normal), ); - Widget toText24({Color? color, bool isBold = false, bool isCenter = false, FontWeight? fontWeight, double? letterSpacing}) => Text( + Widget toText24({Color? color, bool isBold = false, bool isCenter = false, FontWeight? fontWeight, double? letterSpacing, bool isEnglishOnly = false,}) => Text( this, textAlign: isCenter ? TextAlign.center : null, style: TextStyle( + fontFamily: (isEnglishOnly ? "Poppins" : getIt.get().getLanguageCode() == "ar" ? 'GESSTwo' : 'Poppins'), height: 23 / 24, color: color ?? AppColors.blackColor, fontSize: 24.f, letterSpacing: letterSpacing ?? -1, fontWeight: isBold ? FontWeight.bold : fontWeight ?? FontWeight.normal), ); - Widget toText26({Color? color, bool isBold = false, double? height, bool isCenter = false, FontWeight? weight, double? letterSpacing}) => Text( + Widget toText26({Color? color, bool isBold = false, double? height, bool isCenter = false, FontWeight? weight, double? letterSpacing, bool isEnglishOnly = false,}) => Text( this, textAlign: isCenter ? TextAlign.center : null, style: TextStyle( + fontFamily: (isEnglishOnly ? "Poppins" : getIt.get().getLanguageCode() == "ar" ? 'GESSTwo' : 'Poppins'), height: height ?? 23 / 26, color: color ?? AppColors.blackColor, fontSize: 26.f, letterSpacing: letterSpacing ?? -1, fontWeight: weight ?? (isBold ? FontWeight.bold : FontWeight.normal)), ); diff --git a/lib/features/authentication/authentication_repo.dart b/lib/features/authentication/authentication_repo.dart index 74717b8d..eb6e1394 100644 --- a/lib/features/authentication/authentication_repo.dart +++ b/lib/features/authentication/authentication_repo.dart @@ -45,6 +45,8 @@ abstract class AuthenticationRepo { Future>> insertPatientDeviceData({required dynamic patientDeviceDataRequest}); Future>> getPatientDeviceData({required dynamic patientDeviceDataRequest}); + + Future>> getPatientBloodType(); } class AuthenticationRepoImp implements AuthenticationRepo { @@ -656,4 +658,37 @@ class AuthenticationRepoImp implements AuthenticationRepo { } } } + + @override + Future>> getPatientBloodType() async { + Map requestBody = {}; + try { + GenericApiModel? apiResponse; + Failure? failure; + await apiClient.post( + ApiConsts.getPatientBloodTypeNew, + body: requestBody, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: response, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } } diff --git a/lib/features/authentication/authentication_view_model.dart b/lib/features/authentication/authentication_view_model.dart index ba79b70d..8d313774 100644 --- a/lib/features/authentication/authentication_view_model.dart +++ b/lib/features/authentication/authentication_view_model.dart @@ -31,6 +31,7 @@ import 'package:hmg_patient_app_new/features/authentication/models/resp_models/s import 'package:hmg_patient_app_new/features/hmg_services/hmg_services_view_model.dart'; import 'package:hmg_patient_app_new/features/medical_file/medical_file_view_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart'; +import 'package:hmg_patient_app_new/features/symptoms_checker/symptoms_checker_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/authentication/login.dart'; import 'package:hmg_patient_app_new/presentation/authentication/saved_login_screen.dart'; @@ -131,6 +132,8 @@ class AuthenticationViewModel extends ChangeNotifier { pickedCountryByUAEUser = null; _appState.setUserRegistrationPayload = RegistrationDataModelPayload(); _appState.setNHICUserData = CheckUserStatusResponseNHIC(); + getIt.get().setSelectedHeight(0); + getIt.get().setSelectedWeight(0); } void onCountryChange(CountryEnum country) { @@ -624,8 +627,10 @@ class AuthenticationViewModel extends ChangeNotifier { activation.list!.first.zipCode = selectedCountrySignup == CountryEnum.others ? '0' : selectedCountrySignup.countryCode; _appState.setAuthenticatedUser(activation.list!.first); _appState.setPrivilegeModelList(activation.list!.first.listPrivilege!); - // _appState.setUserBloodGroup = activation.patientBlodType ?? "N/A"; - _appState.setUserBloodGroup = activation.patientBloodType ?? "N/A"; + _appState.setUserBloodGroup = activation.patientBlodType ?? "N/A"; + + // Fetch patient blood type from new API + await getPatientBloodTypeNew(); } // _appState.setUserBloodGroup = (activation.patientBlodType ?? ""); _appState.setAppAuthToken = activation.authenticationTokenId; @@ -860,17 +865,17 @@ class AuthenticationViewModel extends ChangeNotifier { resultEither.fold((failure) async => await _errorHandlerService.handleError(failure: failure), (apiResponse) async { if (apiResponse.data is String) { //TODO: This Section Need to Be Testing. - LoadingUtils.hideFullScreenLoader(); + LoaderBottomSheet.hideLoader(); _dialogService.showExceptionBottomSheet(message: apiResponse.data, onOkPressed: () {}, onCancelPressed: () {}); //TODO: Here We Need to Show a Dialog Of Something in the case of Fail With OK and Cancel and the Display Variable WIll be result. } else { - LoadingUtils.hideFullScreenLoader(); + LoaderBottomSheet.hideLoader(); if (apiResponse.data["MessageStatus"] == 1) { - LoadingUtils.showFullScreenLoader(isSuccessDialog: true, loadingText: "Your medical file has been created successfully. \nPlease proceed to login."); + LoaderBottomSheet.showLoader(loadingText: "Your medical file has been created successfully. \nPlease proceed to login."); //TODO: Here We Need to Show a Dialog Of Something in the case of Success. await clearDefaultInputValues(); // This will Clear All Default Values Of User. Future.delayed(Duration(seconds: 1), () { - LoadingUtils.hideFullScreenLoader(); + LoaderBottomSheet.hideLoader(); // _navigationService.pushAndReplace(AppRoutes.loginScreen); _navigationService.pushAndRemoveUntil(CustomPageRoute(page: LandingNavigation()), (r) => false); _navigationService.push(CustomPageRoute(page: LoginScreen())); @@ -1153,4 +1158,29 @@ class AuthenticationViewModel extends ChangeNotifier { _navigationService.pushAndReplace(AppRoutes.landingScreen); } } + + Future getPatientBloodTypeNew() async { + try { + final result = await _authenticationRepo.getPatientBloodType(); + + result.fold( + (failure) async { + // Log error but don't show to user, keep existing blood type + log("Failed to fetch blood type: ${failure.message}"); + }, + (apiResponse) { + if (apiResponse.messageStatus == 1 && apiResponse.data != null) { + // Extract blood type from response + String? bloodType = apiResponse.data['GetPatientBloodType']; + if (bloodType != null && bloodType.isNotEmpty) { + _appState.setUserBloodGroup = bloodType; + log("Blood type updated from new API: $bloodType"); + } + } + }, + ); + } catch (e) { + log("Error calling getPatientBloodType: $e"); + } + } } diff --git a/lib/features/book_appointments/book_appointments_repo.dart b/lib/features/book_appointments/book_appointments_repo.dart index fc541d2e..7a984399 100644 --- a/lib/features/book_appointments/book_appointments_repo.dart +++ b/lib/features/book_appointments/book_appointments_repo.dart @@ -107,6 +107,12 @@ abstract class BookAppointmentsRepo { Function(String)? onError}); Future>> getAppointmentNearestGate({required int projectID, required int clinicID}); + + Future>> isFavouriteDoctor( + {required int patientID, required int projectID, required int clinicID, required int doctorID, Function(dynamic)? onSuccess, Function(String)? onError}); + + Future>> insertFavouriteDoctor( + {required int patientID, required int projectID, required int clinicID, required int doctorID, required bool isActive, Function(dynamic)? onSuccess, Function(String)? onError}); } class BookAppointmentsRepoImp implements BookAppointmentsRepo { @@ -1133,4 +1139,86 @@ class BookAppointmentsRepoImp implements BookAppointmentsRepo { return Left(UnknownFailure(e.toString())); } } + + @override + Future>> isFavouriteDoctor( + {required int patientID, required int projectID, required int clinicID, required int doctorID, Function(dynamic)? onSuccess, Function(String)? onError}) async { + Map mapRequest = {"PatientID": patientID, "ProjectID": projectID, "ClinicID": clinicID, "DoctorID": doctorID}; + + try { + GenericApiModel? apiResponse; + Failure? failure; + await apiClient.post( + IS_FAVOURITE_DOCTOR, + body: mapRequest, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + if (onError != null) { + onError(error); + } + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: response["IsFavouriteDoctor"], + ); + if (onSuccess != null) { + onSuccess(response); + } + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + + @override + Future>> insertFavouriteDoctor( + {required int patientID, required int projectID, required int clinicID, required int doctorID, required bool isActive, Function(dynamic)? onSuccess, Function(String)? onError}) async { + Map mapRequest = {"PatientID": patientID, "ProjectID": projectID, "ClinicID": clinicID, "DoctorID": doctorID, "IsActive": isActive}; + + try { + GenericApiModel? apiResponse; + Failure? failure; + await apiClient.post( + INSERT_FAVOURITE_DOCTOR, + body: mapRequest, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + if (onError != null) { + onError(error); + } + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: response, + ); + if (onSuccess != null) { + onSuccess(response); + } + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } } diff --git a/lib/features/book_appointments/book_appointments_view_model.dart b/lib/features/book_appointments/book_appointments_view_model.dart index 0d2a8f82..327afeb0 100644 --- a/lib/features/book_appointments/book_appointments_view_model.dart +++ b/lib/features/book_appointments/book_appointments_view_model.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:get_it/get_it.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/location_util.dart'; @@ -11,6 +12,7 @@ import 'package:hmg_patient_app_new/core/utils/loading_utils.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_repo.dart'; import 'package:hmg_patient_app_new/features/book_appointments/models/LaserCategoryType.dart'; import 'package:hmg_patient_app_new/features/book_appointments/models/free_slot.dart'; @@ -98,6 +100,8 @@ class BookAppointmentsViewModel extends ChangeNotifier { bool isDoctorRatingDetailsLoading = false; List doctorDetailsList = []; + bool isFavouriteDoctor = false; + List slotsList = []; List docFreeSlots = []; List dayEvents = []; @@ -137,6 +141,7 @@ class BookAppointmentsViewModel extends ChangeNotifier { bool applyFilters = false; bool isWaitingAppointmentAvailable = false; + bool isPatientRescheduleAppointment = false; bool isWaitingAppointmentSelected = false; int waitingAppointmentProjectID = 0; @@ -148,6 +153,72 @@ class BookAppointmentsViewModel extends ChangeNotifier { AppointmentNearestGateResponseModel? appointmentNearestGateResponseModel; ///variables for laser clinic + bool isLaserHospitalsLoading = false; + List laserHospitalsList = []; + int laserHospitalHmgCount = 0; + int laserHospitalHmcCount = 0; + + Future getLaserHospitals({Function(dynamic)? onSuccess, Function(String)? onError}) async { + isLaserHospitalsLoading = true; + laserHospitalsList.clear(); + laserHospitalHmgCount = 0; + laserHospitalHmcCount = 0; + notifyListeners(); + + final result = await bookAppointmentsRepo.getDoctorsList(253, 0, false, 0, ''); + + result.fold( + (failure) async { + isLaserHospitalsLoading = false; + notifyListeners(); + onError?.call(failure.message); + }, + (apiResponse) async { + if (apiResponse.messageStatus == 1) { + var doctorList = apiResponse.data!; + var regionList = await DoctorMapper.getMappedDoctor( + doctorList, + isArabic: _appState.isArabic(), + lat: _appState.userLat, + long: _appState.userLong, + ); + + var isLocationEnabled = (_appState.userLat != 0) && (_appState.userLong != 0); + regionList = await DoctorMapper.sortList(isLocationEnabled, regionList); + + // Flatten all hospitals across all regions into a single list + laserHospitalsList.clear(); + Set addedHospitals = {}; + regionList.registeredDoctorMap?.forEach((region, regionData) { + for (var hospital in regionData?.hmgDoctorList ?? []) { + if (!addedHospitals.contains(hospital.filterName)) { + addedHospitals.add(hospital.filterName ?? ''); + laserHospitalsList.add(hospital); + } + } + for (var hospital in regionData?.hmcDoctorList ?? []) { + if (!addedHospitals.contains(hospital.filterName)) { + addedHospitals.add(hospital.filterName ?? ''); + laserHospitalsList.add(hospital); + } + } + }); + + laserHospitalHmgCount = laserHospitalsList.where((h) => h.isHMC != true).length; + laserHospitalHmcCount = laserHospitalsList.where((h) => h.isHMC == true).length; + + isLaserHospitalsLoading = false; + notifyListeners(); + onSuccess?.call(apiResponse); + } else { + isLaserHospitalsLoading = false; + notifyListeners(); + onError?.call(apiResponse.errorMessage ?? 'Unknown error'); + } + }, + ); + } + List femaleLaserCategory = [ LaserCategoryType(1, 'bodyString'), LaserCategoryType(2, 'face'), @@ -155,7 +226,7 @@ class BookAppointmentsViewModel extends ChangeNotifier { LaserCategoryType(11, 'retouch'), ]; List maleLaserCategory = [ - LaserCategoryType(1, 'body'), + LaserCategoryType(1, 'bodyString'), LaserCategoryType(2, 'face'), LaserCategoryType(11, 'retouch'), ]; @@ -287,6 +358,7 @@ class BookAppointmentsViewModel extends ChangeNotifier { isContinueDentalPlan = false; isChiefComplaintsListLoading = true; isWaitingAppointmentSelected = false; + isPatientRescheduleAppointment = false; bodyTypes = [maleLaserCategory, femaleLaserCategory]; // getLocation(); notifyListeners(); @@ -425,6 +497,11 @@ class BookAppointmentsViewModel extends ChangeNotifier { notifyListeners(); } + setIsPatientRescheduleAppointment(bool isPatientRescheduleAppointment) { + this.isPatientRescheduleAppointment = isPatientRescheduleAppointment; + notifyListeners(); + } + void onTabChanged(int index) { calculationID = null; isGetDocForHealthCal = false; @@ -535,6 +612,9 @@ class BookAppointmentsViewModel extends ChangeNotifier { //TODO: Make the API dynamic with parameters for ProjectID, isNearest, languageID, doctorId, doctorName Future getDoctorsList({int projectID = 0, bool isNearest = true, int doctorId = 0, String doctorName = "", Function(dynamic)? onSuccess, Function(String)? onError}) async { doctorsList.clear(); + filteredDoctorList.clear(); + doctorsListGrouped.clear(); + notifyListeners(); projectID = currentlySelectedHospitalFromRegionFlow != null ? int.parse(currentlySelectedHospitalFromRegionFlow!) : projectID; final result = await bookAppointmentsRepo.getDoctorsList(selectedClinic.clinicID ?? 0, projectID, doctorName.isNotEmpty ? false : isNearest, doctorId, doctorName, isContinueDentalPlan: isContinueDentalPlan); @@ -648,6 +728,17 @@ class BookAppointmentsViewModel extends ChangeNotifier { } else if (apiResponse.messageStatus == 1) { doctorsProfileResponseModel = apiResponse.data!; notifyListeners(); + + // Check if doctor is favorite after getting profile + if(_appState.isAuthenticated) { + checkIsFavouriteDoctor( + patientID: _appState.getAuthenticatedUser()!.patientId!, + projectID: doctorsProfileResponseModel.projectID ?? 0, + clinicID: doctorsProfileResponseModel.clinicID ?? 0, + doctorID: doctorsProfileResponseModel.doctorID ?? 0, + ); + } + if (onSuccess != null) { onSuccess(apiResponse); } @@ -841,64 +932,114 @@ class BookAppointmentsViewModel extends ChangeNotifier { print(failure); onError!(failure.message); }, - (apiResponse) { + (apiResponse) async { if (apiResponse.messageStatus == 2) { // onError!(apiResponse); - LoadingUtils.hideFullScreenLoader(); - showCommonBottomSheetWithoutHeight( - title: LocaleKeys.notice.tr(context: navigationService.navigatorKey.currentContext!), - navigationService.navigatorKey.currentContext!, - child: Utils.getWarningWidget( - loadingText: apiResponse.data["ErrorEndUserMessage"], - isShowActionButtons: true, - onCancelTap: () { - navigationService.pop(); - }, - onConfirmTap: () async { - navigationService.pop(); - PatientAppointmentHistoryResponseModel patientAppointmentHistoryResponseModel = PatientAppointmentHistoryResponseModel( - appointmentNo: apiResponse.data["SameClinicApptList"][0]['AppointmentNo'], - clinicID: apiResponse.data["SameClinicApptList"][0]['ClinicID'], - projectID: apiResponse.data["SameClinicApptList"][0]['ProjectID'], - endDate: apiResponse.data["SameClinicApptList"][0]['EndTime'], - startTime: apiResponse.data["SameClinicApptList"][0]['StartTime'], - doctorID: apiResponse.data["SameClinicApptList"][0]['DoctorID'], - isLiveCareAppointment: apiResponse.data["SameClinicApptList"][0]['IsLiveCareAppointment'], - originalClinicID: 0, - originalProjectID: 0, - appointmentDate: apiResponse.data["SameClinicApptList"][0]['AppointmentDate'], - ); - - LoaderBottomSheet.showLoader( - loadingText: LocaleKeys.reschedulingAppo.tr(context: navigationService.navigatorKey.currentContext!), - ); - await cancelAppointment(patientAppointmentHistoryResponseModel: patientAppointmentHistoryResponseModel).then((val) async { + if (isPatientRescheduleAppointment) { + PatientAppointmentHistoryResponseModel patientAppointmentHistoryResponseModel = PatientAppointmentHistoryResponseModel( + appointmentNo: apiResponse.data["SameClinicApptList"][0]['AppointmentNo'], + clinicID: apiResponse.data["SameClinicApptList"][0]['ClinicID'], + projectID: apiResponse.data["SameClinicApptList"][0]['ProjectID'], + endDate: apiResponse.data["SameClinicApptList"][0]['EndTime'], + startTime: apiResponse.data["SameClinicApptList"][0]['StartTime'], + doctorID: apiResponse.data["SameClinicApptList"][0]['DoctorID'], + isLiveCareAppointment: apiResponse.data["SameClinicApptList"][0]['IsLiveCareAppointment'], + originalClinicID: 0, + originalProjectID: 0, + appointmentDate: apiResponse.data["SameClinicApptList"][0]['AppointmentDate'], + ); + + await cancelAppointment(patientAppointmentHistoryResponseModel: patientAppointmentHistoryResponseModel).then((val) async { + // LoaderBottomSheet.hideLoader(); + Future.delayed(Duration(milliseconds: 50)).then((value) async {}); + // LoaderBottomSheet.showLoader(loadingText: LocaleKeys.bookingYourAppointment.tr()); + await insertSpecificAppointment( + onError: (err) {}, + onSuccess: (apiResp) async { LoaderBottomSheet.hideLoader(); - Future.delayed(Duration(milliseconds: 50)).then((value) async {}); - LoadingUtils.showFullScreenLoader(barrierDismissible: true, isSuccessDialog: false, loadingText: LocaleKeys.bookingYourAppointment.tr()); - await insertSpecificAppointment( - onError: (err) {}, - onSuccess: (apiResp) async { - LoadingUtils.hideFullScreenLoader(); - await Future.delayed(Duration(milliseconds: 50)).then((value) async { - LoadingUtils.showFullScreenLoader(barrierDismissible: true, isSuccessDialog: true, loadingText: LocaleKeys.appointmentSuccess.tr()); - await Future.delayed(Duration(milliseconds: 4000)).then((value) { - LoadingUtils.hideFullScreenLoader(); - Navigator.pushAndRemoveUntil( - navigationService.navigatorKey.currentContext!, - CustomPageRoute( - page: LandingNavigation(), - ), - (r) => false); + await Future.delayed(Duration(milliseconds: 50)).then((value) async { + showCommonBottomSheetWithoutHeight( + GetIt.instance().navigatorKey.currentContext!, + child: Utils.getSuccessWidget(loadingText: LocaleKeys.appointmentSuccess.tr()).paddingSymmetrical(0.h, 24.h), + callBackFunc: () { + setIsPatientRescheduleAppointment(false); + Navigator.pushAndRemoveUntil( + navigationService.navigatorKey.currentContext!, + CustomPageRoute( + page: LandingNavigation(), + ), + (r) => false); + }, + isFullScreen: false, + isCloseButtonVisible: false, + isAutoDismiss: true + ); + }); + }); + }); + } else { + LoaderBottomSheet.hideLoader(); + showCommonBottomSheetWithoutHeight( + title: LocaleKeys.notice.tr(context: navigationService.navigatorKey.currentContext!), + navigationService.navigatorKey.currentContext!, + child: Utils.getWarningWidget( + loadingText: apiResponse.data["ErrorEndUserMessage"], + isShowActionButtons: true, + onCancelTap: () { + navigationService.pop(); + }, + onConfirmTap: () async { + navigationService.pop(); + PatientAppointmentHistoryResponseModel patientAppointmentHistoryResponseModel = PatientAppointmentHistoryResponseModel( + appointmentNo: apiResponse.data["SameClinicApptList"][0]['AppointmentNo'], + clinicID: apiResponse.data["SameClinicApptList"][0]['ClinicID'], + projectID: apiResponse.data["SameClinicApptList"][0]['ProjectID'], + endDate: apiResponse.data["SameClinicApptList"][0]['EndTime'], + startTime: apiResponse.data["SameClinicApptList"][0]['StartTime'], + doctorID: apiResponse.data["SameClinicApptList"][0]['DoctorID'], + isLiveCareAppointment: apiResponse.data["SameClinicApptList"][0]['IsLiveCareAppointment'], + originalClinicID: 0, + originalProjectID: 0, + appointmentDate: apiResponse.data["SameClinicApptList"][0]['AppointmentDate'], + ); + + LoaderBottomSheet.showLoader( + loadingText: LocaleKeys.reschedulingAppo.tr(context: navigationService.navigatorKey.currentContext!), + ); + await cancelAppointment(patientAppointmentHistoryResponseModel: patientAppointmentHistoryResponseModel).then((val) async { + LoaderBottomSheet.hideLoader(); + Future.delayed(Duration(milliseconds: 50)).then((value) async {}); + LoaderBottomSheet.showLoader(loadingText: LocaleKeys.bookingYourAppointment.tr()); + await insertSpecificAppointment( + onError: (err) {}, + onSuccess: (apiResp) async { + LoaderBottomSheet.hideLoader(); + await Future.delayed(Duration(milliseconds: 50)).then((value) async { + showCommonBottomSheetWithoutHeight( + GetIt.instance().navigatorKey.currentContext!, + child: Utils.getSuccessWidget(loadingText: LocaleKeys.appointmentSuccess.tr()).paddingSymmetrical(0.h, 24.h), + callBackFunc: () { + setIsPatientRescheduleAppointment(false); + Navigator.pushAndRemoveUntil( + navigationService.navigatorKey.currentContext!, + CustomPageRoute( + page: LandingNavigation(), + ), + (r) => false); + }, + isFullScreen: false, + isCloseButtonVisible: false, + isAutoDismiss: true + ); }); }); - }); - }); - }), - callBackFunc: () {}, - isFullScreen: false, - isCloseButtonVisible: true, - ); + }); + }), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + } } else if (apiResponse.messageStatus == 1) { if (apiResponse.data == null || apiResponse.data!.isEmpty) { onError!("No free slots available".tr()); @@ -935,7 +1076,7 @@ class BookAppointmentsViewModel extends ChangeNotifier { (apiResponse) { if (apiResponse.messageStatus == 2) { // onError!(apiResponse); - LoadingUtils.hideFullScreenLoader(); + LoaderBottomSheet.hideLoader(); showCommonBottomSheetWithoutHeight( title: LocaleKeys.notice.tr(context: navigationService.navigatorKey.currentContext!), navigationService.navigatorKey.currentContext!, @@ -971,22 +1112,28 @@ class BookAppointmentsViewModel extends ChangeNotifier { await cancelAppointment(patientAppointmentHistoryResponseModel: patientAppointmentHistoryResponseModel).then((val) async { navigationService.pop(); Future.delayed(Duration(milliseconds: 50)).then((value) async {}); - LoadingUtils.showFullScreenLoader(barrierDismissible: true, isSuccessDialog: false, loadingText: LocaleKeys.bookingYourAppointment.tr()); + LoaderBottomSheet.showLoader(loadingText: LocaleKeys.bookingYourAppointment.tr()); await insertSpecificAppointment( onError: (err) {}, onSuccess: (apiResp) async { - LoadingUtils.hideFullScreenLoader(); + LoaderBottomSheet.hideLoader(); await Future.delayed(Duration(milliseconds: 50)).then((value) async { - LoadingUtils.showFullScreenLoader(barrierDismissible: true, isSuccessDialog: true, loadingText: LocaleKeys.appointmentSuccess.tr()); - await Future.delayed(Duration(milliseconds: 4000)).then((value) { - LoadingUtils.hideFullScreenLoader(); - Navigator.pushAndRemoveUntil( - navigationService.navigatorKey.currentContext!, - CustomPageRoute( - page: LandingNavigation(), - ), - (r) => false); - }); + showCommonBottomSheetWithoutHeight( + GetIt.instance().navigatorKey.currentContext!, + child: Utils.getSuccessWidget(loadingText: LocaleKeys.appointmentSuccess.tr()).paddingSymmetrical(0.h, 24.h), + callBackFunc: () { + setIsPatientRescheduleAppointment(false); + Navigator.pushAndRemoveUntil( + navigationService.navigatorKey.currentContext!, + CustomPageRoute( + page: LandingNavigation(), + ), + (r) => false); + }, + isFullScreen: false, + isCloseButtonVisible: false, + isAutoDismiss: true + ); }); }); }); @@ -1480,7 +1627,7 @@ class BookAppointmentsViewModel extends ChangeNotifier { }, (apiResponse) { if (apiResponse.messageStatus == 2) { - LoadingUtils.hideFullScreenLoader(); + LoaderBottomSheet.hideLoader(); showCommonBottomSheetWithoutHeight( title: LocaleKeys.notice.tr(context: navigationService.navigatorKey.currentContext!), navigationService.navigatorKey.currentContext!, @@ -1530,4 +1677,79 @@ class BookAppointmentsViewModel extends ChangeNotifier { }, ); } + + void toggleFavouriteDoctor() { + isFavouriteDoctor = !isFavouriteDoctor; + notifyListeners(); + } + + void setIsFavouriteDoctor(bool value) { + isFavouriteDoctor = value; + notifyListeners(); + } + + Future checkIsFavouriteDoctor({required int patientID, required int projectID, required int clinicID, required int doctorID, Function(dynamic)? onSuccess, Function(String)? onError}) async { + final result = await bookAppointmentsRepo.isFavouriteDoctor( + patientID: patientID, + projectID: projectID, + clinicID: clinicID, + doctorID: doctorID, + onSuccess: onSuccess, + onError: onError, + ); + + result.fold( + (failure) async { + if (onError != null) { + onError(failure.message); + } + }, + (apiResponse) { + if (apiResponse.messageStatus == 2) { + if (onError != null) { + onError(apiResponse.errorMessage ?? "Failed to check favorite doctor"); + } + } else if (apiResponse.messageStatus == 1) { + // Check the response for IsFavouriteDoctor flag + bool isFavorite = apiResponse.data; + setIsFavouriteDoctor(isFavorite); + if (onSuccess != null) { + onSuccess(apiResponse.data); + } + } + }, + ); + } + + Future insertFavouriteDoctor({required int patientID, required int projectID, required int clinicID, required int doctorID, required bool isActive, Function(dynamic)? onSuccess, Function(String)? onError}) async { + final result = await bookAppointmentsRepo.insertFavouriteDoctor( + patientID: patientID, + projectID: projectID, + clinicID: clinicID, + doctorID: doctorID, + isActive: isActive, + onSuccess: onSuccess, + onError: onError, + ); + + result.fold( + (failure) async { + if (onError != null) { + onError(failure.message); + } + }, + (apiResponse) { + if (apiResponse.messageStatus == 2) { + if (onError != null) { + onError(apiResponse.errorMessage ?? "Failed to update favorite doctor"); + } + } else if (apiResponse.messageStatus == 1) { + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse.data); + } + } + }, + ); + } } diff --git a/lib/features/book_appointments/models/resp_models/get_favorite_doctors_list.dart b/lib/features/book_appointments/models/resp_models/get_favorite_doctors_list.dart new file mode 100644 index 00000000..b3048e67 --- /dev/null +++ b/lib/features/book_appointments/models/resp_models/get_favorite_doctors_list.dart @@ -0,0 +1,81 @@ +import 'dart:convert'; + +class GetFavoriteDoctorsListModel { + int? id; + int? projectId; + int? clinicId; + int? doctorId; + int? patientId; + bool? patientOutSa; + bool? isActive; + String? createdOn; + dynamic modifiedOn; + String? doctorImageUrl; + String? doctorName; + String? doctorTitle; + String? nationalityFlagUrl; + String? nationalityId; + String? nationalityName; + List? speciality; + + GetFavoriteDoctorsListModel({ + this.id, + this.projectId, + this.clinicId, + this.doctorId, + this.patientId, + this.patientOutSa, + this.isActive, + this.createdOn, + this.modifiedOn, + this.doctorImageUrl, + this.doctorName, + this.doctorTitle, + this.nationalityFlagUrl, + this.nationalityId, + this.nationalityName, + this.speciality, + }); + + factory GetFavoriteDoctorsListModel.fromRawJson(String str) => GetFavoriteDoctorsListModel.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + factory GetFavoriteDoctorsListModel.fromJson(Map json) => GetFavoriteDoctorsListModel( + id: json["ID"], + projectId: json["ProjectID"], + clinicId: json["ClinicID"], + doctorId: json["DoctorID"], + patientId: json["PatientID"], + patientOutSa: json["PatientOutSA"], + isActive: json["IsActive"], + createdOn: json["CreatedOn"], + modifiedOn: json["ModifiedOn"], + doctorImageUrl: json["DoctorImageURL"], + doctorName: json["DoctorName"], + doctorTitle: json["DoctorTitle"], + nationalityFlagUrl: json["NationalityFlagURL"], + nationalityId: json["NationalityID"], + nationalityName: json["NationalityName"], + speciality: json["Speciality"] == null ? [] : List.from(json["Speciality"]!.map((x) => x)), + ); + + Map toJson() => { + "ID": id, + "ProjectID": projectId, + "ClinicID": clinicId, + "DoctorID": doctorId, + "PatientID": patientId, + "PatientOutSA": patientOutSa, + "IsActive": isActive, + "CreatedOn": createdOn, + "ModifiedOn": modifiedOn, + "DoctorImageURL": doctorImageUrl, + "DoctorName": doctorName, + "DoctorTitle": doctorTitle, + "NationalityFlagURL": nationalityFlagUrl, + "NationalityID": nationalityId, + "NationalityName": nationalityName, + "Speciality": speciality == null ? [] : List.from(speciality!.map((x) => x)), + }; +} diff --git a/lib/features/book_appointments/models/resp_models/laser_body_parts.dart b/lib/features/book_appointments/models/resp_models/laser_body_parts.dart index d7d3fbd1..dcb8a70e 100644 --- a/lib/features/book_appointments/models/resp_models/laser_body_parts.dart +++ b/lib/features/book_appointments/models/resp_models/laser_body_parts.dart @@ -61,4 +61,14 @@ class LaserBodyPart { data['CategoryNameN'] = this.categoryNameN; return data; } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is LaserBodyPart && + runtimeType == other.runtimeType && + id == other.id; + + @override + int get hashCode => id.hashCode; } diff --git a/lib/features/emergency_services/emergency_services_view_model.dart b/lib/features/emergency_services/emergency_services_view_model.dart index ec211793..bfe226f8 100644 --- a/lib/features/emergency_services/emergency_services_view_model.dart +++ b/lib/features/emergency_services/emergency_services_view_model.dart @@ -530,6 +530,7 @@ class EmergencyServicesViewModel extends ChangeNotifier { void setTransportationOption(PatientERTransportationMethod item) { selectedTransportOption = item; + notifyListeners(); } void updateCallingPlace(AmbulanceCallingPlace? value) { diff --git a/lib/features/hmg_services/hmg_services_repo.dart b/lib/features/hmg_services/hmg_services_repo.dart index 89285339..b70e024c 100644 --- a/lib/features/hmg_services/hmg_services_repo.dart +++ b/lib/features/hmg_services/hmg_services_repo.dart @@ -919,8 +919,6 @@ class HmgServicesRepoImp implements HmgServicesRepo { @override Future>>> getPatientVitalSign() async { Map requestBody = { - - }; try { diff --git a/lib/features/hmg_services/models/ui_models/vital_sign_ui_model.dart b/lib/features/hmg_services/models/ui_models/vital_sign_ui_model.dart index 328ffc93..66f5d77b 100644 --- a/lib/features/hmg_services/models/ui_models/vital_sign_ui_model.dart +++ b/lib/features/hmg_services/models/ui_models/vital_sign_ui_model.dart @@ -48,13 +48,14 @@ class VitalSignUiModel { ); } - if (s.contains('low')) { - final Color yellowBg = AppColors.warningColor.withValues(alpha: 0.12); + // Warning for both low and overweight/underweight BMI, since they can indicate potential health issues. + if (s.contains('low') || s.contains('underweight') || s.contains('overweight')) { + final Color yellowBg = AppColors.highAndLow.withValues(alpha: 0.12); return VitalSignUiModel( iconBg: yellowBg, - iconFg: AppColors.warningColor, + iconFg: AppColors.highAndLow, chipBg: yellowBg, - chipFg: AppColors.warningColor, + chipFg: AppColors.highAndLow, ); } @@ -91,12 +92,27 @@ class VitalSignUiModel { } static String bmiStatus(dynamic bmi) { - if (bmi == null) return 'N/A'; - final double bmiValue = double.tryParse(bmi.toString()) ?? 0; - if (bmiValue < 18.5) return 'Underweight'; - if (bmiValue < 25) return 'Normal'; - if (bmiValue < 30) return 'Overweight'; - return 'High'; + String bmiStatus = 'Normal'; + final double bmiResult = double.tryParse(bmi.toString()) ?? 0; + + if (bmiResult >= 30) { + bmiStatus = "High"; + } else if (bmiResult < 30 && bmiResult >= 25) { + bmiStatus = "Overweight"; + } else if (bmiResult < 25 && bmiResult >= 18.5) { + bmiStatus = "Normal"; + } else if (bmiResult < 18.5) { + bmiStatus = "Underweight"; + } + + // if (bmi == null) return 'N/A'; + // final double bmiValue = double.tryParse(bmi.toString()) ?? 0; + // if (bmiValue < 18.5) return 'Underweight'; + // if (bmiValue < 25) return 'Normal'; + // if (bmiValue < 30) return 'Overweight'; + // return 'High'; + + return bmiStatus; } } diff --git a/lib/features/lab/lab_view_model.dart b/lib/features/lab/lab_view_model.dart index 1af2654e..de543970 100644 --- a/lib/features/lab/lab_view_model.dart +++ b/lib/features/lab/lab_view_model.dart @@ -4,6 +4,7 @@ import 'dart:core'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/common_models/data_points.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/utils/date_util.dart'; @@ -71,6 +72,9 @@ class LabViewModel extends ChangeNotifier { List uniqueTestsList = []; List indexedCharacterForUniqueTest = []; + List filteredUniqueTestsList = []; + List filteredIndexedCharacterForUniqueTest = []; + double maxY = 0.0; double minY = double.infinity; double maxX = double.infinity; @@ -84,6 +88,9 @@ class LabViewModel extends ChangeNotifier { LabOrderResponseByAi? labOrderResponseByAi; LabOrdersResponseByAi? labOrdersResponseByAi; + bool isLabAIAnalysisNeedsToBeShown = true; + bool isLabResultsHistoryShowMore = false; + LabViewModel({required this.labRepo, required this.errorHandlerService, required this.navigationService}); initLabProvider() { @@ -93,6 +100,8 @@ class LabViewModel extends ChangeNotifier { labOrderTests.clear(); isLabOrdersLoading = true; isLabResultsLoading = true; + isLabAIAnalysisNeedsToBeShown = true; + isLabResultsHistoryShowMore = false; patientLabOrdersByClinic.clear(); patientLabOrdersByHospital.clear(); patientLabOrdersViewList.clear(); @@ -111,6 +120,16 @@ class LabViewModel extends ChangeNotifier { notifyListeners(); } + setIsLabAIAnalysisNeedsToBeShown(bool isLabAIAnalysisNeedsToBeShown) { + this.isLabAIAnalysisNeedsToBeShown = isLabAIAnalysisNeedsToBeShown; + notifyListeners(); + } + + setIsLabResultsHistoryShowMore() { + isLabResultsHistoryShowMore = !isLabResultsHistoryShowMore; + notifyListeners(); + } + void setIsSortByClinic(bool value) { isSortByClinic = value; patientLabOrdersViewList = isSortByClinic ? patientLabOrdersByClinic : patientLabOrdersByHospital; @@ -218,7 +237,8 @@ class LabViewModel extends ChangeNotifier { final clinicMap = >{}; final hospitalMap = >{}; if (query.isEmpty) { - // filteredLabOrders = List.from(patientLabOrders); // reset + filteredLabOrders = List.from(patientLabOrders); // reset + filteredUniqueTestsList = List.from(uniqueTestsList); for (var order in patientLabOrders) { final clinicKey = (order.clinicDescription ?? 'Unknown').trim(); clinicMap.putIfAbsent(clinicKey, () => []).add(order); @@ -234,7 +254,10 @@ class LabViewModel extends ChangeNotifier { final descriptions = order.testDetails?.map((d) => d.description?.toLowerCase()).toList() ?? []; return descriptions.any((desc) => desc != null && desc.contains(query.toLowerCase())); }).toList(); - // patientLabOrders = filteredLabOrders; + filteredUniqueTestsList = uniqueTestsList.where((test) { + final desc = test.description?.toLowerCase() ?? ''; + return desc.contains(query.toLowerCase()); + }).toList(); for (var order in filteredLabOrders) { final clinicKey = (order.clinicDescription ?? 'Unknown').trim(); clinicMap.putIfAbsent(clinicKey, () => []).add(order); @@ -246,6 +269,14 @@ class LabViewModel extends ChangeNotifier { patientLabOrdersByHospital = hospitalMap.values.toList(); patientLabOrdersViewList = isSortByClinic ? patientLabOrdersByClinic : patientLabOrdersByHospital; } + // Rebuild filtered indexed characters + filteredIndexedCharacterForUniqueTest = []; + for (var test in filteredUniqueTestsList) { + String label = test.description ?? ""; + if (label.isEmpty) continue; + if (filteredIndexedCharacterForUniqueTest.contains(label[0].toLowerCase())) continue; + filteredIndexedCharacterForUniqueTest.add(label[0].toLowerCase()); + } notifyListeners(); } @@ -280,6 +311,10 @@ class LabViewModel extends ChangeNotifier { for (var element in uniqueTests) { labOrderTests.add(element.description ?? ""); } + + // Initialize filtered lists with full data + filteredUniqueTestsList = List.from(uniqueTestsList); + filteredIndexedCharacterForUniqueTest = List.from(indexedCharacterForUniqueTest); } Future getLabResultsByAppointmentNo( @@ -387,7 +422,8 @@ class LabViewModel extends ChangeNotifier { LoaderBottomSheet.hideLoader(); if (apiResponse.messageStatus == 2) { } else if (apiResponse.messageStatus == 1) { - var recentThree = sort(apiResponse.data!); + var sortedResult = sort(apiResponse.data!); + var recentThree = sortedResult.take(3).toList(); mainLabResults = recentThree; double highRefrenceValue = double.negativeInfinity; @@ -395,11 +431,12 @@ class LabViewModel extends ChangeNotifier { double lowRefenceValue = double.infinity; String? flagForLowReferenceRange; - recentThree.reversed.forEach((element) { + sortedResult.toList().reversed.forEach((element) { try { var dateTime = DateUtil.convertStringToDate(element.verifiedOnDateTime!); var resultValue = double.parse(element.resultValue!); - var transformedValue = transformValueInRange(double.parse(element.resultValue!), element.calculatedResultFlag ?? ""); + // var transformedValue = transformValueInRange(double.parse(element.resultValue!), element.calculatedResultFlag ?? ""); + var transformedValue = resultValue; if (resultValue > maxY) { maxY = resultValue; maxX = maxY; @@ -431,9 +468,9 @@ class LabViewModel extends ChangeNotifier { highRefrenceValue = maxY; lowRefenceValue = minY; } - + // if (minY > lowRefenceValue) { - minY = lowRefenceValue - 25; + minY = lowRefenceValue - getInterval(); } this.flagForHighReferenceRange = flagForHighReferenceRange; @@ -442,12 +479,16 @@ class LabViewModel extends ChangeNotifier { lowTransformedReferenceValue = double.parse(transformValueInRange(lowRefenceValue, flagForLowReferenceRange ?? "").toStringAsFixed(1)); this.highRefrenceValue = double.parse(highRefrenceValue.toStringAsFixed(1)); this.lowRefenceValue = double.parse(lowRefenceValue.toStringAsFixed(1)); - - if (maxY < highRefrenceValue) { + if(maxY=1.0 && maxX < 5.0) return .3; + if(maxX >=5.0 && maxX < 10.0) return 1.5; + if(maxX >=10.0 && maxX < 50.0) return 2.5; + if(maxX >=50.0 && maxX < 100.0) return 5; + if(maxX >=100.0 && maxX < 200.0) return 10; + return 15; + } void checkIfGraphShouldBeDisplayed(LabResult recentResult) { shouldShowGraph = recentResult.checkIfGraphShouldBeDisplayed(); @@ -586,7 +638,8 @@ class LabViewModel extends ChangeNotifier { try { var dateTime = DateUtil.convertStringToDate(element.verifiedOnDateTime!); var resultValue = double.parse(element.resultValue!); - var transformedValue = transformValueInRange(double.parse(element.resultValue!), element.calculatedResultFlag ?? ""); + // var transformedValue = transformValueInRange(double.parse(element.resultValue!), element.calculatedResultFlag ?? ""); + var transformedValue = double.parse(element.resultValue!); if (resultValue > maxY) { maxY = resultValue; } @@ -769,7 +822,13 @@ class LabViewModel extends ChangeNotifier { Future getAiOverviewLabOrders({required PatientLabOrdersResponseModel labOrder, required String loadingText}) async { // LoadingUtils.showFullScreenLoader(loadingText: "Loading and analysing your data,\nPlease be patient and let the AI do the magic. \nPlease be patient, This might take some time."); - LoadingUtils.showFullScreenLoader(loadingText: loadingText); + // LoadingUtils.showFullScreenLoader(loadingText: loadingText); + LoaderBottomSheet.showLoader( + loadingText: loadingText, + showCloseButton: true, + onCloseTap: () { + setIsLabAIAnalysisNeedsToBeShown(false); + }); List> results = []; Map orderData = {"order_date": labOrder.orderDate ?? "", "clinic": labOrder.clinicDescription ?? "", "doctor": labOrder.doctorName ?? "", "results": []}; List> testResults = []; @@ -792,16 +851,20 @@ class LabViewModel extends ChangeNotifier { result.fold( (failure) async { LoadingUtils.hideFullScreenLoader(); - await errorHandlerService.handleError(failure: failure); + if (isLabAIAnalysisNeedsToBeShown) { + await errorHandlerService.handleError(failure: failure); + } }, (apiResponse) { LoadingUtils.hideFullScreenLoader(); if (apiResponse.messageStatus == 2) { } else if (apiResponse.messageStatus == 1) { - labOrdersResponseByAi = apiResponse.data; - navigationService.push( - MaterialPageRoute(builder: (_) => LabAiAnalysisDetailedPage()), - ); + if (isLabAIAnalysisNeedsToBeShown) { + labOrdersResponseByAi = apiResponse.data; + navigationService.push( + MaterialPageRoute(builder: (_) => LabAiAnalysisDetailedPage()), + ); + } } }, ); @@ -810,7 +873,12 @@ class LabViewModel extends ChangeNotifier { } Future getAiOverviewSingleLabResult({required String langId, required LabResult recentLabResult, required String loadingText}) async { - LoaderBottomSheet.showLoader(loadingText: loadingText); + LoaderBottomSheet.showLoader( + loadingText: loadingText, + showCloseButton: true, + onCloseTap: () { + setIsLabAIAnalysisNeedsToBeShown(false); + }); List> results = []; results.add({ "Description": recentLabResult.description ?? '', @@ -820,19 +888,24 @@ class LabViewModel extends ChangeNotifier { "ReferanceRange": recentLabResult.referanceRange ?? '', }); - var payload = {"patient_id": currentlySelectedPatientOrder!.patientID, "language_id": langId, "lab_results": results}; + // var payload = {"patient_id": currentlySelectedPatientOrder!.patientID, "language_id": langId, "lab_results": results}; + var payload = {"patient_id": getIt.get().getAuthenticatedUser()!.patientId, "language_id": langId, "lab_results": results}; final result = await labRepo.getPatientAiOverViewLabOrder(payload); result.fold( (failure) async { LoaderBottomSheet.hideLoader(); - await errorHandlerService.handleError(failure: failure); + if (isLabAIAnalysisNeedsToBeShown) { + await errorHandlerService.handleError(failure: failure); + } }, (apiResponse) { LoaderBottomSheet.hideLoader(); if (apiResponse.messageStatus == 2) { } else if (apiResponse.messageStatus == 1) { - labOrderResponseByAi = apiResponse.data; - notifyListeners(); + if (isLabAIAnalysisNeedsToBeShown) { + labOrderResponseByAi = apiResponse.data; + notifyListeners(); + } } }, ); diff --git a/lib/features/my_appointments/appointment_via_region_viewmodel.dart b/lib/features/my_appointments/appointment_via_region_viewmodel.dart index c5dcaf62..1144be82 100644 --- a/lib/features/my_appointments/appointment_via_region_viewmodel.dart +++ b/lib/features/my_appointments/appointment_via_region_viewmodel.dart @@ -43,9 +43,20 @@ class AppointmentViaRegionViewmodel extends ChangeNotifier { int hmgCount = 0; int hmcCount = 0; RegionBottomSheetType regionBottomSheetType = RegionBottomSheetType.FOR_REGION; + bool sortByLocation = false; AppointmentViaRegionViewmodel({required this.navigationService,required this.appState}); + void initSortByLocation() { + sortByLocation = (appState.userLat != 0.0) && (appState.userLong != 0.0); + notifyListeners(); + } + + void setSortByLocation(bool value) { + sortByLocation = value; + notifyListeners(); + } + void setSelectedRegionId(String? regionId) { selectedRegionId = regionId; notifyListeners(); @@ -76,6 +87,8 @@ class AppointmentViaRegionViewmodel extends ChangeNotifier { hmcCount = registeredDoctorMap.hmcSize; hmgCount = registeredDoctorMap.hmgSize; + hospitalList!.sort((a, b) => num.parse(a.distanceInKMs!).compareTo(num.parse(b.distanceInKMs!))); + getDisplayList(); } @@ -122,6 +135,7 @@ class AppointmentViaRegionViewmodel extends ChangeNotifier { setFacility(null); setBottomSheetType(RegionBottomSheetType.FOR_REGION); setBottomSheetState(AppointmentViaRegionState.REGION_SELECTION); + sortByLocation = false; } void setHospitalModel(PatientDoctorAppointmentList? hospital) { diff --git a/lib/features/my_appointments/models/resp_models/patient_appointment_history_response_model.dart b/lib/features/my_appointments/models/resp_models/patient_appointment_history_response_model.dart index e1ae67d6..c0da3c47 100644 --- a/lib/features/my_appointments/models/resp_models/patient_appointment_history_response_model.dart +++ b/lib/features/my_appointments/models/resp_models/patient_appointment_history_response_model.dart @@ -73,6 +73,7 @@ class PatientAppointmentHistoryResponseModel { num? patientShare; num? patientShareWithTax; num? patientTaxAmount; + String? doctorNationalityFlagURL; PatientAppointmentHistoryResponseModel({ this.setupID, @@ -148,6 +149,7 @@ class PatientAppointmentHistoryResponseModel { this.patientShare, this.patientShareWithTax, this.patientTaxAmount, + this.doctorNationalityFlagURL, }); PatientAppointmentHistoryResponseModel.fromJson(Map json) { @@ -235,6 +237,7 @@ class PatientAppointmentHistoryResponseModel { patientShare = json['PatientShare']; patientShareWithTax = json['PatientShareWithTax']; patientTaxAmount = json['PatientTaxAmount']; + doctorNationalityFlagURL = json['DoctorNationalityFlagURL']; } Map toJson() { diff --git a/lib/features/my_appointments/my_appointments_repo.dart b/lib/features/my_appointments/my_appointments_repo.dart index 567714c6..4c7c41e2 100644 --- a/lib/features/my_appointments/my_appointments_repo.dart +++ b/lib/features/my_appointments/my_appointments_repo.dart @@ -8,6 +8,7 @@ import 'package:hmg_patient_app_new/core/common_models/generic_api_model.dart'; import 'package:hmg_patient_app_new/core/exceptions/api_failure.dart'; import 'package:hmg_patient_app_new/core/utils/date_util.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/get_favorite_doctors_list.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/rate_appointment_resp_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/ask_doctor_request_type_response_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/get_tamara_installments_details_response_model.dart'; @@ -51,6 +52,8 @@ abstract class MyAppointmentsRepo { Future>>> getPatientDoctorsList(); + Future>>> getFavouriteDoctorsList(); + Future>> insertLiveCareVIDARequest({required clientRequestID, required PatientAppointmentHistoryResponseModel patientAppointmentHistoryResponseModel}); Future>> getTamaraInstallmentsDetails(); @@ -510,7 +513,10 @@ class MyAppointmentsRepoImp implements MyAppointmentsRepo { try { final list = response['PatientDoctorAppointmentResultList']; - final appointmentsList = list.map((item) => PatientAppointmentHistoryResponseModel.fromJson(item as Map)).toList().cast(); + List appointmentsList = + list.map((item) => PatientAppointmentHistoryResponseModel.fromJson(item as Map)).toList().cast(); + + // appointmentsList.removeWhere((element) => element.isActiveDoctorProfile == false); apiResponse = GenericApiModel>( messageStatus: messageStatus, @@ -531,6 +537,56 @@ class MyAppointmentsRepoImp implements MyAppointmentsRepo { } } + @override + Future>>> getFavouriteDoctorsList() async { + Map mapDevice = {}; + + try { + GenericApiModel>? apiResponse; + Failure? failure; + await apiClient.post( + GET_FAVOURITE_DOCTOR, + body: mapDevice, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + final list = response['Patient_GetFavouriteDoctorList']; + + if (list == null || list.isEmpty) { + apiResponse = GenericApiModel>( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: [], + ); + return; + } + + final appointmentsList = (list as List).map((item) => GetFavoriteDoctorsListModel.fromJson(item as Map)).toList().cast(); + + appointmentsList.removeWhere((element) => element.isActive == false); + + apiResponse = GenericApiModel>( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: appointmentsList, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } + @override Future> insertLiveCareVIDARequest({required clientRequestID, required PatientAppointmentHistoryResponseModel patientAppointmentHistoryResponseModel}) async { Map requestBody = { diff --git a/lib/features/my_appointments/my_appointments_view_model.dart b/lib/features/my_appointments/my_appointments_view_model.dart index 1584f891..a1c48867 100644 --- a/lib/features/my_appointments/my_appointments_view_model.dart +++ b/lib/features/my_appointments/my_appointments_view_model.dart @@ -5,6 +5,7 @@ import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/utils/date_util.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/get_favorite_doctors_list.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/appointemnet_filters.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/ask_doctor_request_type_response_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/get_tamara_installments_details_response_model.dart'; @@ -37,6 +38,8 @@ class MyAppointmentsViewModel extends ChangeNotifier { bool isAppointmentPatientShareLoading = false; bool isTimeLineAppointmentsLoading = false; bool isPatientMyDoctorsLoading = false; + bool isPatientFavouriteDoctorsLoading = false; + bool isFavouriteDoctorsDataFetched = false; bool isAppointmentDataToBeLoaded = true; @@ -64,6 +67,8 @@ class MyAppointmentsViewModel extends ChangeNotifier { List patientMyDoctorsList = []; + List patientFavouriteDoctorsList = []; + List patientEyeMeasurementsAppointmentsHistoryList = []; // Grouping by Clinic and Hospital @@ -89,6 +94,10 @@ class MyAppointmentsViewModel extends ChangeNotifier { selectedTabIndex = index; start = null; end = null; + // if (index == 0) { + // filteredAppointmentList.clear(); + // filteredAppointmentList.addAll(patientAppointmentsHistoryList); + // } notifyListeners(); } @@ -659,6 +668,51 @@ class MyAppointmentsViewModel extends ChangeNotifier { ); } + Future getPatientFavouriteDoctors({bool forceRefresh = false, Function(dynamic)? onSuccess, Function(String)? onError}) async { + // If data is already fetched and not forcing refresh, skip API call + if (isFavouriteDoctorsDataFetched && !forceRefresh) { + return; + } + + isPatientFavouriteDoctorsLoading = true; + patientFavouriteDoctorsList.clear(); + notifyListeners(); + + final result = await myAppointmentsRepo.getFavouriteDoctorsList(); + + result.fold( + (failure) async { + isPatientFavouriteDoctorsLoading = false; + notifyListeners(); + }, + (apiResponse) { + if (apiResponse.messageStatus == 2) { + isPatientFavouriteDoctorsLoading = false; + notifyListeners(); + } else if (apiResponse.messageStatus == 1) { + patientFavouriteDoctorsList = apiResponse.data!; + isFavouriteDoctorsDataFetched = true; + isPatientFavouriteDoctorsLoading = false; + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse); + } + } + }, + ); + } + + // Method to force refresh favorite doctors list + void refreshFavouriteDoctors() { + isFavouriteDoctorsDataFetched = false; + getPatientFavouriteDoctors(forceRefresh: true); + } + + // Method to reset favorite doctors cache + void resetFavouriteDoctorsCache() { + isFavouriteDoctorsDataFetched = false; + } + Future insertLiveCareVIDARequest( {required clientRequestID, required PatientAppointmentHistoryResponseModel patientAppointmentHistoryResponseModel, Function(dynamic)? onSuccess, Function(String)? onError}) async { final result = await myAppointmentsRepo.insertLiveCareVIDARequest(clientRequestID: clientRequestID, patientAppointmentHistoryResponseModel: patientAppointmentHistoryResponseModel); diff --git a/lib/features/notifications/notifications_repo.dart b/lib/features/notifications/notifications_repo.dart index 853a9792..a4588042 100644 --- a/lib/features/notifications/notifications_repo.dart +++ b/lib/features/notifications/notifications_repo.dart @@ -12,6 +12,10 @@ abstract class NotificationsRepo { required int pagingSize, required int currentPage, }); + + Future>>> markAsRead({ + required int notificationID, + }); } class NotificationsRepoImp implements NotificationsRepo { @@ -75,4 +79,38 @@ class NotificationsRepoImp implements NotificationsRepo { return Left(UnknownFailure(e.toString())); } } + + @override + Future>>> markAsRead({required int notificationID}) async { + Map mapDevice = {"NotificationPoolID": notificationID}; + + try { + GenericApiModel>? apiResponse; + Failure? failure; + await apiClient.post( + PUSH_NOTIFICATION_SET_MESSAGES_FROM_POOL_AS_READ, + body: mapDevice, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + apiResponse = GenericApiModel>( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: null, + data: [], + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } } diff --git a/lib/features/notifications/notifications_view_model.dart b/lib/features/notifications/notifications_view_model.dart index e15e267c..85049ec9 100644 --- a/lib/features/notifications/notifications_view_model.dart +++ b/lib/features/notifications/notifications_view_model.dart @@ -7,13 +7,15 @@ class NotificationsViewModel extends ChangeNotifier { bool isNotificationsLoading = false; bool hasMoreNotifications = true; + int unreadNotificationsCount = 0; + NotificationsRepo notificationsRepo; ErrorHandlerService errorHandlerService; List notificationsList = []; int currentPage = 0; - int pagingSize = 14; + int pagingSize = 50; int notificationStatusID = 2; // Default to status 2 (e.g., unread) NotificationsViewModel({ @@ -46,6 +48,7 @@ class NotificationsViewModel extends ChangeNotifier { Function(String)? onError, }) async { isNotificationsLoading = true; + unreadNotificationsCount = 0; notifyListeners(); final result = await notificationsRepo.getAllNotifications( @@ -78,6 +81,11 @@ class NotificationsViewModel extends ChangeNotifier { } notificationsList.addAll(newNotifications); + for (var notification in notificationsList) { + if (notification.isRead == false) { + unreadNotificationsCount++; + } + } currentPage++; notifyListeners(); @@ -89,6 +97,21 @@ class NotificationsViewModel extends ChangeNotifier { ); } + Future markAsRead({ + int notificationID = 0, + Function(dynamic)? onSuccess, + Function(String)? onError, + }) async { + final result = await notificationsRepo.markAsRead(notificationID: notificationID); + + result.fold( + (failure) async {}, + (apiResponse) { + refreshNotifications(); + }, + ); + } + Future refreshNotifications({ Function(dynamic)? onSuccess, Function(String)? onError, diff --git a/lib/features/prescriptions/prescriptions_view_model.dart b/lib/features/prescriptions/prescriptions_view_model.dart index 4baf6882..b9c29072 100644 --- a/lib/features/prescriptions/prescriptions_view_model.dart +++ b/lib/features/prescriptions/prescriptions_view_model.dart @@ -52,6 +52,8 @@ class PrescriptionsViewModel extends ChangeNotifier { bool isPrescriptionsDataNeedsReloading = true; List prescriptionsOrderList = []; + DateTime? selectedReminderTime; + PrescriptionsViewModel({required this.prescriptionsRepo, required this.errorHandlerService, required this.navServices}); initPrescriptionsViewModel() { @@ -71,6 +73,7 @@ class PrescriptionsViewModel extends ChangeNotifier { Future checkIfReminderExistForPrescription(int index) async { prescriptionDetailsList[index].hasReminder = await CalenderUtilsNew.instance.checkIfEventExist(prescriptionDetailsList[index].itemID?.toString() ?? ""); + notifyListeners(); return prescriptionDetailsList[index].hasReminder ?? false; } @@ -308,4 +311,9 @@ class PrescriptionsViewModel extends ChangeNotifier { }, ); } + + void serSelectedTime(DateTime dateTime) { + selectedReminderTime = dateTime; + notifyListeners(); + } } diff --git a/lib/features/profile_settings/models/get_patient_info_response_model.dart b/lib/features/profile_settings/models/get_patient_info_response_model.dart new file mode 100644 index 00000000..ea89c9f9 --- /dev/null +++ b/lib/features/profile_settings/models/get_patient_info_response_model.dart @@ -0,0 +1,136 @@ +class GetPatientInfoForUpdate { + int? projectID; + int? patientType; + int? patientID; + String? emailAddress; + bool? isEmailAlertRequired; + bool? isSMSAlertRequired; + String? preferredLanguage; + String? emergencyContactName; + String? emergencyContactNo; + int? editedBy; + String? editedOn; + String? patientIdentificationNo; + int? patientIdentificationType; + String? firstName; + String? middleName; + String? lastName; + int? gender; + String? dateofBirth; + String? dateofBirthN; + String? firstNameN; + String? middleNameN; + String? lastNameN; + int? projectID1; + int? patientType1; + int? patientID1; + bool? isIVRStopped; + String? aGE; + String? genderString; + bool? isNeedUpdateIdintificationNo; + String? nationality; + String? type; + + GetPatientInfoForUpdate( + {this.projectID, + this.patientType, + this.patientID, + this.emailAddress, + this.isEmailAlertRequired, + this.isSMSAlertRequired, + this.preferredLanguage, + this.emergencyContactName, + this.emergencyContactNo, + this.editedBy, + this.editedOn, + this.patientIdentificationNo, + this.patientIdentificationType, + this.firstName, + this.middleName, + this.lastName, + this.gender, + this.dateofBirth, + this.dateofBirthN, + this.firstNameN, + this.middleNameN, + this.lastNameN, + this.projectID1, + this.patientType1, + this.patientID1, + this.isIVRStopped, + this.aGE, + this.genderString, + this.isNeedUpdateIdintificationNo, + this.nationality, + this.type}); + + GetPatientInfoForUpdate.fromJson(Map json) { + projectID = json['ProjectID']; + patientType = json['PatientType']; + patientID = json['PatientID']; + emailAddress = json['EmailAddress']; + isEmailAlertRequired = json['IsEmailAlertRequired']; + isSMSAlertRequired = json['IsSMSAlertRequired']; + preferredLanguage = json['PreferredLanguage']; + emergencyContactName = json['EmergencyContactName']; + emergencyContactNo = json['EmergencyContactNo']; + editedBy = json['EditedBy']; + editedOn = json['EditedOn']; + patientIdentificationNo = json['PatientIdentificationNo']; + patientIdentificationType = json['PatientIdentificationType']; + firstName = json['FirstName']; + middleName = json['MiddleName']; + lastName = json['LastName']; + gender = json['Gender']; + dateofBirth = json['DateofBirth']; + dateofBirthN = json['DateofBirthN']; + firstNameN = json['FirstNameN']; + middleNameN = json['MiddleNameN']; + lastNameN = json['LastNameN']; + projectID1 = json['ProjectID1']; + patientType1 = json['PatientType1']; + patientID1 = json['PatientID1']; + isIVRStopped = json['IsIVRStopped']; + aGE = json['AGE']; + genderString = json['GenderString']; + isNeedUpdateIdintificationNo = json['IsNeedUpdateIdintificationNo']; + nationality = json['Nationality']; + type = json['Type']; + } + + Map toJson() { + final Map data = new Map(); + data['ProjectID'] = this.projectID; + data['PatientType'] = this.patientType; + data['PatientID'] = this.patientID; + data['EmailAddress'] = this.emailAddress; + data['IsEmailAlertRequired'] = this.isEmailAlertRequired; + data['IsSMSAlertRequired'] = this.isSMSAlertRequired; + data['PreferredLanguage'] = this.preferredLanguage; + data['EmergencyContactName'] = this.emergencyContactName; + data['EmergencyContactNo'] = this.emergencyContactNo; + data['EditedBy'] = this.editedBy; + data['EditedOn'] = this.editedOn; + data['PatientIdentificationNo'] = this.patientIdentificationNo; + data['PatientIdentificationType'] = this.patientIdentificationType; + data['FirstName'] = this.firstName; + data['MiddleName'] = this.middleName; + data['LastName'] = this.lastName; + data['Gender'] = this.gender; + data['DateofBirth'] = this.dateofBirth; + data['DateofBirthN'] = this.dateofBirthN; + data['FirstNameN'] = this.firstNameN; + data['MiddleNameN'] = this.middleNameN; + data['LastNameN'] = this.lastNameN; + data['ProjectID1'] = this.projectID1; + data['PatientType1'] = this.patientType1; + data['PatientID1'] = this.patientID1; + data['IsIVRStopped'] = this.isIVRStopped; + data['AGE'] = this.aGE; + data['GenderString'] = this.genderString; + data['IsNeedUpdateIdintificationNo'] = this.isNeedUpdateIdintificationNo; + data['Nationality'] = this.nationality; + data['Type'] = this.type; + return data; + } +} diff --git a/lib/features/profile_settings/profile_settings_repo.dart b/lib/features/profile_settings/profile_settings_repo.dart index 253bf505..9d4c1221 100644 --- a/lib/features/profile_settings/profile_settings_repo.dart +++ b/lib/features/profile_settings/profile_settings_repo.dart @@ -3,6 +3,7 @@ import 'package:hmg_patient_app_new/core/api/api_client.dart'; import 'package:hmg_patient_app_new/core/api_consts.dart'; import 'package:hmg_patient_app_new/core/common_models/generic_api_model.dart'; import 'package:hmg_patient_app_new/core/exceptions/api_failure.dart'; +import 'package:hmg_patient_app_new/features/profile_settings/models/get_patient_info_response_model.dart'; import 'package:hmg_patient_app_new/services/logger_service.dart'; abstract class ProfileSettingsRepo { @@ -13,6 +14,8 @@ abstract class ProfileSettingsRepo { /// Deactivates (deletes) the patient's account. Future>> deactivateAccount(); + + Future>> getProfileSettings(); } class ProfileSettingsRepoImp implements ProfileSettingsRepo { @@ -96,5 +99,43 @@ class ProfileSettingsRepoImp implements ProfileSettingsRepo { return Left(UnknownFailure(e.toString())); } } + + @override + Future> getProfileSettings() async { + final Map body = {}; + + try { + GenericApiModel? apiResponse; + Failure? failure; + + await apiClient.post( + PROFILE_SETTING, + body: body, + onFailure: (error, statusCode, {messageStatus, failureType}) { + failure = failureType; + }, + onSuccess: (response, statusCode, {messageStatus, errorMessage}) { + try { + final patientProfileInfo = GetPatientInfoForUpdate.fromJson(response['PateintInfoForUpdateList'][0]); + + apiResponse = GenericApiModel( + messageStatus: messageStatus, + statusCode: statusCode, + errorMessage: errorMessage, + data: patientProfileInfo, + ); + } catch (e) { + failure = DataParsingFailure(e.toString()); + } + }, + ); + + if (failure != null) return Left(failure!); + if (apiResponse == null) return Left(ServerFailure("Unknown error")); + return Right(apiResponse!); + } catch (e) { + return Left(UnknownFailure(e.toString())); + } + } } diff --git a/lib/features/profile_settings/profile_settings_view_model.dart b/lib/features/profile_settings/profile_settings_view_model.dart index 92f11528..28f28af6 100644 --- a/lib/features/profile_settings/profile_settings_view_model.dart +++ b/lib/features/profile_settings/profile_settings_view_model.dart @@ -1,8 +1,16 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; +import 'package:get_it/get_it.dart'; +import 'package:hmg_patient_app_new/features/profile_settings/models/get_patient_info_response_model.dart'; import 'package:hmg_patient_app_new/features/profile_settings/profile_settings_repo.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/presentation/profile_settings/widgets/preferred_language_widget.dart'; import 'package:hmg_patient_app_new/services/cache_service.dart'; import 'package:hmg_patient_app_new/services/error_handler_service.dart'; +import 'package:hmg_patient_app_new/services/navigation_service.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; class ProfileSettingsViewModel extends ChangeNotifier { static const String _darkModeKey = 'is_dark_mode'; @@ -30,6 +38,9 @@ class ProfileSettingsViewModel extends ChangeNotifier { bool isDeactivateAccountSuccess = false; String? deactivateAccountError; + late GetPatientInfoForUpdate getPatientInfoForUpdate; + bool isPatientProfileLoading = false; + ProfileSettingsViewModel({ required CacheService cacheService, required this.profileSettingsRepo, @@ -88,6 +99,34 @@ class ProfileSettingsViewModel extends ChangeNotifier { ); } + Future getProfileSettings({ + Function(dynamic)? onSuccess, + Function(String)? onError, + }) async { + isPatientProfileLoading = true; + notifyListeners(); + + final result = await profileSettingsRepo.getProfileSettings(); + + result.fold( + (failure) { + isPatientProfileLoading = false; + notifyListeners(); + if (onError != null) { + onError(failure.message); + } else { + errorHandlerService.handleError(failure: failure); + } + }, + (response) { + getPatientInfoForUpdate = response.data; + isPatientProfileLoading = false; + notifyListeners(); + onSuccess?.call(response.data); + }, + ); + } + // ── Deactivate account ─────────────────────────────────────────────── Future deactivateAccount({ @@ -123,7 +162,14 @@ class ProfileSettingsViewModel extends ChangeNotifier { // ── Helpers ────────────────────────────────────────────────────────── - void notify() { - notifyListeners(); + openPreferredLanguageBottomSheet() { + showCommonBottomSheetWithoutHeight( + title: LocaleKeys.preferredLanguage.tr(), + GetIt.instance().navigatorKey.currentContext!, + child: PreferredLanguageWidget(), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); } } diff --git a/lib/features/smartwatch_health_data/HealthDataTransformation.dart b/lib/features/smartwatch_health_data/HealthDataTransformation.dart new file mode 100644 index 00000000..ffda4f4f --- /dev/null +++ b/lib/features/smartwatch_health_data/HealthDataTransformation.dart @@ -0,0 +1,134 @@ +import 'dart:math'; + +import 'package:hmg_patient_app_new/core/common_models/data_points.dart'; +import 'package:intl/intl.dart'; + +import 'model/Vitals.dart'; + +enum Durations { + daily("daily"), + weekly("weekly"), + monthly("monthly"), + halfYearly("halfYearly"), + yearly("yearly"); + + final String value; + const Durations(this.value); +} + +class HealthDataTransformation { + Map> transformVitalsToDataPoints(VitalsWRTType vitals, String filterType, String selectedSection,) { + final Map> dataPointMap = {}; + Map> data = vitals.getVitals(); + // Group data based on the filter type + Map> groupedData = {}; + // List > items = data.values.toList(); + List keys = data.keys.toList(); + var count = 0; + List item = data[selectedSection] ?? []; + // for(var item in items) { + List dataPoints = []; + + for (var vital in item) { + String key = ""; + if (vital.value == "" || vital.timestamp == "") continue; + var parseDate = DateTime.parse(vital.timestamp); + var currentDate = normalizeToStartOfDay(DateTime.now()); + if (filterType == Durations.daily.value) { + if(isBetweenInclusive(parseDate, currentDate, DateTime.now())) { + key = DateFormat('yyyy-MM-dd HH').format(DateTime.parse(vital.timestamp)); + groupedData.putIfAbsent(key, () => []).add(vital); + }// Group by hour + } else if (filterType == Durations.weekly.value) { + if(isBetweenInclusive(parseDate, currentDate.subtract(Duration(days: 7)), DateTime.now())) { + key = DateFormat('yyyy-MM-dd').format(DateTime.parse(vital.timestamp)); + groupedData.putIfAbsent(key, () => []).add(vital); + + } // Group by day + } else if (filterType == Durations.monthly.value) { + if(isBetweenInclusive(parseDate, currentDate.subtract(Duration(days: 30)), DateTime.now())) { + print("the value for the monthly filter is ${vital.value} with the timestamp ${vital.timestamp} and the current date is $currentDate and the parse date is $parseDate"); + key = DateFormat('yyyy-MM-dd').format(DateTime.parse(vital.timestamp)); + groupedData.putIfAbsent(key, () => []).add(vital); + + } // Group by day + } else if (filterType == Durations.halfYearly.value || filterType == Durations.yearly.value) { + if(isBetweenInclusive(parseDate, currentDate.subtract(Duration(days: filterType == Durations.halfYearly.value?180: 365)), DateTime.now())) { + key = DateFormat('yyyy-MM').format(DateTime.parse(vital.timestamp)); + groupedData.putIfAbsent(key, () => []).add(vital); + + } // Group by month + } else { + throw ArgumentError('Invalid filter type'); + } + } + print("the size of groupData is ${groupedData.values.length}"); + + // Process grouped data + groupedData.forEach((key, values) { + double sum = values.fold(0, (acc, v) => acc + num.parse(v.value)); + double mean = sum / values.length; + if(selectedSection == "bodyOxygen" || selectedSection == "bodyTemperature") { + mean = sum / values.length; + }else { + mean = sum; + } + + double finalValue = mean; + print("the final value is $finalValue for the key $key with the original values ${values.map((v) => v.value).toList()} and uom is ${values.first.unitOfMeasure}"); + dataPoints.add(DataPoint( + value: smartScale(finalValue), + label: key, + actualValue: finalValue.toStringAsFixed(2), + displayTime: key, + unitOfMeasurement:values.first.unitOfMeasure , + time: DateTime.parse(values.first.timestamp), + )); + }); + + dataPointMap[filterType] = dataPoints; + // } + return dataPointMap; + } + + double smartScale(double number) { + // if (number <= 0) return 0; + // final _random = Random(); + // double ratio = number / 100; + // + // double scalingFactor = ratio > 1 ? 100 / number : 100; + // + // double result = (number / 100) * scalingFactor; + // print("the ratio is ${ratio.toInt()+1}"); + // double max = (100+_random.nextInt(ratio.toInt()+10)).toDouble(); + // + // return result.clamp(0, max); + + if (number <= 0) return 0; + + final random = Random(); + + // Smooth compression scaling + double baseScaled = number <20 ? number:100 * (number / (number + 100)); + + // Random factor between 0.9 and 1.1 (±10%) + double randomFactor = number <20 ? random.nextDouble() * 1.5: 0.9 + random.nextDouble() * 0.2; + + double result = baseScaled * randomFactor; + + return result.clamp(0, 100); + } + + DateTime normalizeToStartOfDay(DateTime date) { + return DateTime(date.year, date.month, date.day); + } + bool isBetweenInclusive( + DateTime target, + DateTime start, + DateTime end, + ) { + return !normalizeToStartOfDay(target).isBefore(start) && !normalizeToStartOfDay(target).isAfter(end); + } + + +} \ No newline at end of file diff --git a/lib/features/smartwatch_health_data/health_provider.dart b/lib/features/smartwatch_health_data/health_provider.dart index 0acb25bb..963c0352 100644 --- a/lib/features/smartwatch_health_data/health_provider.dart +++ b/lib/features/smartwatch_health_data/health_provider.dart @@ -1,6 +1,18 @@ import 'package:flutter/foundation.dart'; import 'package:health/health.dart'; +import 'package:hmg_patient_app_new/core/common_models/smart_watch.dart'; +import 'package:hmg_patient_app_new/core/utils/date_util.dart'; +import 'package:hmg_patient_app_new/core/utils/loading_utils.dart'; import 'package:hmg_patient_app_new/features/smartwatch_health_data/health_service.dart'; +import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; + +import '../../core/common_models/data_points.dart'; +import '../../core/dependencies.dart'; +import '../../presentation/smartwatches/activity_detail.dart' show ActivityDetails; +import '../../presentation/smartwatches/smart_watch_activity.dart' show SmartWatchActivity; +import '../../services/navigation_service.dart' show NavigationService; +import 'HealthDataTransformation.dart'; +import 'model/Vitals.dart'; class HealthProvider with ChangeNotifier { final HealthService _healthService = HealthService(); @@ -10,13 +22,27 @@ class HealthProvider with ChangeNotifier { String selectedTimeRange = '7D'; int selectedTabIndex = 0; - String selectedWatchType = 'apple'; + SmartWatchTypes? selectedWatchType ; String selectedWatchURL = 'assets/images/png/smartwatches/apple-watch-5.jpg'; + HealthDataTransformation healthDataTransformation = HealthDataTransformation(); + + String selectedSection = ""; + Map> daily = {}; + Map> weekly = {}; + Map> monthly = {}; + Map> halgyearly = {}; + Map> yearly = {}; + Map> selectedData = {}; + Durations selectedDuration = Durations.daily; + VitalsWRTType? vitals; + double? averageValue; + String? averageValueString; - setSelectedWatchType(String type, String imageURL) { + setSelectedWatchType(SmartWatchTypes type, String imageURL) { selectedWatchType = type; selectedWatchURL = imageURL; notifyListeners(); + _healthService.addWatchHelper(type); } void onTabChanged(int index) { @@ -40,9 +66,7 @@ class HealthProvider with ChangeNotifier { final startTime = _getStartDate(); final endTime = DateTime.now(); - healthData = await _healthService.getAllHealthData(startTime, endTime); - isLoading = false; notifyListeners(); } catch (e) { @@ -91,4 +115,176 @@ class HealthProvider with ChangeNotifier { return DateTime.now().subtract(const Duration(days: 7)); } } + + void initDevice() async { + LoaderBottomSheet.showLoader(); + notifyListeners(); + final result = await _healthService.initDevice(); + isLoading = false; + LoaderBottomSheet.hideLoader(); + if (result.isError) { + error = 'Error initializing device: ${result.asError}'; + } else { + LoaderBottomSheet.showLoader(); + await getVitals(); + // LoaderBottomSheet.hideLoader(); + // await Future.delayed(Duration(seconds: 5)); + getIt.get().pushPage(page: SmartWatchActivity()); + print('Device initialized successfully'); + } + notifyListeners(); + } + + Future getVitals() async { + + final result = await _healthService.getVitals(); + vitals = result; + LoaderBottomSheet.hideLoader(); + + notifyListeners(); + } + + mapValuesForFilters( + Durations filter, + String selectedSection, + ) { + if (vitals == null) return {}; + + switch (filter) { + case Durations.daily: + if (daily.isNotEmpty) { + selectedData = daily; + break; + } + selectedData = daily = healthDataTransformation.transformVitalsToDataPoints(vitals!, Durations.daily.value, selectedSection); + break; + case Durations.weekly: + if (weekly.isNotEmpty) { + selectedData = weekly; + break; + } + selectedData = weekly = healthDataTransformation.transformVitalsToDataPoints(vitals!, Durations.weekly.value, selectedSection); + break; + case Durations.monthly: + if (monthly.isNotEmpty) { + selectedData = monthly; + break; + } + selectedData = monthly = healthDataTransformation.transformVitalsToDataPoints(vitals!, Durations.monthly.value, selectedSection); + break; + case Durations.halfYearly: + if (halgyearly.isNotEmpty) { + selectedData = halgyearly; + break; + } + selectedData = halgyearly = healthDataTransformation.transformVitalsToDataPoints(vitals!, Durations.halfYearly.value, selectedSection); + break; + case Durations.yearly: + if (yearly.isNotEmpty) { + selectedData = yearly; + break; + } + selectedData = yearly = healthDataTransformation.transformVitalsToDataPoints(vitals!, Durations.yearly.value, selectedSection); + break; + default: + {} + ; + } + notifyListeners(); + } + + void navigateToDetails(String value, {required String sectionName, required String uom}) { + getIt.get().pushPage(page: ActivityDetails(selectedActivity: value, sectionName:sectionName, uom: uom,)); + } + + void saveSelectedSection(String value) { + // if(selectedSection == value) return; + selectedSection = value; + } + + void deleteDataIfSectionIsDifferent(String value) { + // if(selectedSection == value){ + // return; + // } + daily.clear(); + weekly.clear(); + halgyearly.clear(); + monthly.clear(); + yearly.clear(); + selectedSection = ""; + selectedSection = ""; + averageValue = null; + averageValueString = null; + selectedDuration = Durations.daily; + } + + void fetchData() { + // if(selectedSection == value) return; + mapValuesForFilters(selectedDuration, selectedSection); + getAverageForData(); + transformValueIfSleepIsSelected(); + } + + void setDurations(Durations duration) { + selectedDuration = duration; + } + + void getAverageForData() { + if (selectedData.isEmpty) { + averageValue = 0.0; + notifyListeners(); + return; + } + double total = 0; + int count = 0; + selectedData.forEach((key, dataPoints) { + for (var dataPoint in dataPoints) { + total += num.parse(dataPoint.actualValue); + count++; + } + }); + print("total count is $count and total is $total"); + averageValue = count > 0 ? total / count : null; + notifyListeners(); + } + + void transformValueIfSleepIsSelected() { + if (selectedSection != "sleep") return; + if (averageValue == null) { + averageValueString = null; + return; + } + averageValueString = DateUtil.millisToHourMin(averageValue?.toInt() ?? 0); + averageValue = null; + notifyListeners(); + } + + String firstNonEmptyValue(List dataPoints) { + try { + return dataPoints.firstWhere((dp) => dp.value != null && dp.value!.trim().isNotEmpty).value; + } catch (e) { + return "0"; // no non-empty value found + } + } + + String sumOfNonEmptyData(List list) { + final now = DateTime.now().toLocal(); + final today = DateTime(now.year, now.month, now.day); + + var data = double.parse(list + .where((dp) { + final localDate = DateTime.parse(dp.timestamp); + final normalized = DateTime(localDate.year, localDate.month, localDate.day); + + return normalized.isAtSameMomentAs(today); + }) + .fold("0", (sum, dp) => (double.parse(sum) + double.parse(dp.value)).toString()) + .toString()); + var formattedString = data.toStringAsFixed(2); + + if (formattedString.endsWith('.00')) { + return formattedString.substring(0, formattedString.length - 3); + } + return formattedString; + } } diff --git a/lib/features/smartwatch_health_data/health_service.dart b/lib/features/smartwatch_health_data/health_service.dart index d3815b3b..7d42092d 100644 --- a/lib/features/smartwatch_health_data/health_service.dart +++ b/lib/features/smartwatch_health_data/health_service.dart @@ -1,9 +1,17 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:developer'; import 'dart:io'; import 'package:health/health.dart'; +import 'package:hmg_patient_app_new/core/common_models/smart_watch.dart'; +import 'package:hmg_patient_app_new/features/smartwatch_health_data/model/Vitals.dart'; +import 'package:hmg_patient_app_new/features/smartwatch_health_data/watch_connectors/create_watch_helper.dart'; +import 'package:hmg_patient_app_new/features/smartwatch_health_data/watch_connectors/watch_helper.dart'; import 'package:permission_handler/permission_handler.dart'; import 'health_utils.dart'; +import 'package:async/async.dart'; class HealthService { static final HealthService _instance = HealthService._internal(); @@ -14,6 +22,8 @@ class HealthService { final Health health = Health(); + WatchHelper? watchHelper; + final List _healthMetrics = [ HealthDataType.HEART_RATE, // HealthDataType.STEPS, @@ -161,4 +171,42 @@ class HealthService { return []; } } + + void addWatchHelper(SmartWatchTypes watchType){ + watchHelper = CreateWatchHelper.getWatchName(watchType) ; + } + + Future> initDevice() async { + if(watchHelper == null){ + return Result.error('No watch helper found'); + } + return await watchHelper!.initDevice(); + } + + Future getVitals() async { + if (watchHelper == null) { + print('No watch helper found'); + return null; + } + try { + await watchHelper!.getHeartRate(); + await watchHelper!.getSleep(); + await watchHelper!.getSteps(); + await watchHelper!.getActivity(); + await watchHelper!.getBodyTemperature(); + await watchHelper!.getDistance(); + await watchHelper!.getBloodOxygen(); + Result data = await watchHelper!.retrieveData(); + if(data.isError) { + print('Unable to get the data'); + } + var response = jsonDecode(data.asValue?.value?.toString()?.trim().replaceAll("\n", "")??""); + VitalsWRTType vitals = VitalsWRTType.fromMap(response); + log("the data is ${vitals}"); + return vitals; + }catch(e){ + print('Error getting heart rate: $e'); + } + return null; + } } diff --git a/lib/features/smartwatch_health_data/model/Vitals.dart b/lib/features/smartwatch_health_data/model/Vitals.dart new file mode 100644 index 00000000..96386f2d --- /dev/null +++ b/lib/features/smartwatch_health_data/model/Vitals.dart @@ -0,0 +1,103 @@ +class Vitals { + String value; + final String timestamp; + final String unitOfMeasure; + + Vitals({ + required this.value, + required this.timestamp, + this.unitOfMeasure = "", + }); + + factory Vitals.fromMap(Map map) { + return Vitals( + value: map['value'] ?? "", + timestamp: map['timeStamp'] ?? "", + unitOfMeasure: map['uom'] ?? "", + ); + } + + + toString(){ + return "{\"value\": \"$value\", \"timeStamp\": \"$timestamp\", \"uom\": \"$unitOfMeasure\"}"; + } +} + +class VitalsWRTType { + final List heartRate; + final List sleep; + final List step; + final List distance; + final List activity; + final List bodyOxygen; + final List bodyTemperature; + double maxHeartRate = double.negativeInfinity; + double maxSleep = double.negativeInfinity; + double maxStep= double.negativeInfinity; + double maxActivity = double.negativeInfinity; + double maxBloodOxygen = double.negativeInfinity; + double maxBodyTemperature = double.negativeInfinity; + + + VitalsWRTType({required this.distance, required this.bodyOxygen, required this.bodyTemperature, required this.heartRate, required this.sleep, required this.step, required this.activity}); + + factory VitalsWRTType.fromMap(Map map) { + List activity = []; + List steps = []; + List sleeps = []; + List heartRate = []; + List bodyOxygen = []; + List distance = []; + List bodyTemperature = []; + map["activity"].forEach((element) { + element["uom"] = "Kcal"; + var data = Vitals.fromMap(element); + activity.add(data); + }); + map["steps"].forEach((element) { + element["uom"] = ""; + + steps.add(Vitals.fromMap(element)); + }); + map["sleep"].forEach((element) { + element["uom"] = "hr"; + sleeps.add(Vitals.fromMap(element)); + }); + map["heartRate"].forEach((element) { + element["uom"] = "bpm"; + + heartRate.add(Vitals.fromMap(element)); + }); + map["bloodOxygen"].forEach((element) { + element["uom"] = ""; + + bodyOxygen.add(Vitals.fromMap(element)); + }); + + map["bodyTemperature"].forEach((element) { + element["uom"] = "C"; + bodyTemperature.add(Vitals.fromMap(element)); + }); + + map["distance"].forEach((element) { + element["uom"] = "km"; + var data = Vitals.fromMap(element); + data.value = (double.parse(data.value)/1000).toStringAsFixed(2); + distance.add(data); + }); + + return VitalsWRTType(bodyTemperature: bodyTemperature, bodyOxygen: bodyOxygen, heartRate: heartRate, sleep: sleeps, step: steps, activity: activity, distance: distance); + } + + Map> getVitals() { + return { + "heartRate": heartRate , + "sleep": sleep, + "steps": step, + "activity": activity, + "bodyOxygen": bodyOxygen, + "bodyTemperature": bodyTemperature, + "distance": distance, + }; + } +} diff --git a/lib/features/smartwatch_health_data/platform_channel/samsung_platform_channel.dart b/lib/features/smartwatch_health_data/platform_channel/samsung_platform_channel.dart new file mode 100644 index 00000000..a36cda39 --- /dev/null +++ b/lib/features/smartwatch_health_data/platform_channel/samsung_platform_channel.dart @@ -0,0 +1,90 @@ + +import 'dart:async'; + +import 'package:async/async.dart'; +import 'package:flutter/services.dart'; +class SamsungPlatformChannel { + final MethodChannel _channel = MethodChannel('samsung_watch'); + Future> initDevice() async { + try{ + await _channel.invokeMethod('init'); + return Result.value(true); + }catch(e){ + return Result.error(e); + } + } + + Future> getRequestedPermission() async { + try{ + await _channel.invokeMethod('getPermission'); + return Result.value(true); + }catch(e){ + return Result.error(e); + } + } + Future> getHeartRate() async { + try{ + await _channel.invokeMethod('getHeartRate'); + return Result.value(true); + }catch(e){ + return Result.error(e); + } + } + Future> getSleep() async { + try{ + await _channel.invokeMethod('getSleepData'); + return Result.value(true); + }catch(e){ + return Result.error(e); + } + } + Future> getSteps() async { + try{ + await _channel.invokeMethod('steps'); + return Result.value(true); + }catch(e){ + return Result.error(e); + } + } + + Future> getActivity() async { + try{ + await _channel.invokeMethod('activitySummary'); + return Result.value(true); + }catch(e){ + return Result.error(e); + } + } + + Future> retrieveData() async { + try{ + return Result.value(await _channel.invokeMethod('retrieveData')); + }catch(e){ + return Result.error(e); + } + } + + Future> getBloodOxygen() async { + try{ + return Result.value(await _channel.invokeMethod('bloodOxygen')); + }catch(e){ + return Result.error(e); + } + } + + Future> getBodyTemperature() async { + try{ + return Result.value(await _channel.invokeMethod('bodyTemperature')); + }catch(e){ + return Result.error(e); + } + } + + Future> getDistance() async { + try{ + return Result.value(await _channel.invokeMethod('distance')); + }catch(e){ + return Result.error(e); + } + } +} \ No newline at end of file diff --git a/lib/features/smartwatch_health_data/watch_connectors/create_watch_helper.dart b/lib/features/smartwatch_health_data/watch_connectors/create_watch_helper.dart new file mode 100644 index 00000000..8abb2136 --- /dev/null +++ b/lib/features/smartwatch_health_data/watch_connectors/create_watch_helper.dart @@ -0,0 +1,22 @@ +import 'dart:io'; + +import 'package:hmg_patient_app_new/core/common_models/smart_watch.dart'; +import 'package:hmg_patient_app_new/features/smartwatch_health_data/watch_connectors/health_connect_helper.dart'; +import 'package:hmg_patient_app_new/features/smartwatch_health_data/watch_connectors/huawei_watch_connecter.dart'; +import 'package:hmg_patient_app_new/features/smartwatch_health_data/watch_connectors/samsung_health.dart'; +import 'package:hmg_patient_app_new/features/smartwatch_health_data/watch_connectors/watch_helper.dart'; + +class CreateWatchHelper { + static WatchHelper getWatchName(SmartWatchTypes watchType) { + /// if running device is ios + if(Platform.isIOS) return HealthConnectHelper(); + switch(watchType){ + case SmartWatchTypes.samsung: + return SamsungHealth(); + case SmartWatchTypes.huawei: + return HuaweiHealthDataConnector(); + default: + return SamsungHealth(); + } + } +} \ No newline at end of file diff --git a/lib/features/smartwatch_health_data/watch_connectors/health_connect_helper.dart b/lib/features/smartwatch_health_data/watch_connectors/health_connect_helper.dart new file mode 100644 index 00000000..87e1cf3a --- /dev/null +++ b/lib/features/smartwatch_health_data/watch_connectors/health_connect_helper.dart @@ -0,0 +1,188 @@ +import 'dart:async'; +import 'dart:io'; +import 'package:async/src/result/result.dart'; +import 'package:flutter/material.dart'; +import 'package:health/health.dart'; +import 'package:hmg_patient_app_new/features/smartwatch_health_data/watch_connectors/watch_helper.dart' show WatchHelper; +import 'package:permission_handler/permission_handler.dart'; + +import '../model/Vitals.dart'; + +class HealthConnectHelper extends WatchHelper { + final Health health = Health(); + + final List _healthPermissions = [ + HealthDataType.ACTIVE_ENERGY_BURNED, + HealthDataType.HEART_RATE, + HealthDataType.STEPS, + HealthDataType.BLOOD_OXYGEN, + HealthDataType.BODY_TEMPERATURE, + HealthDataType.DISTANCE_WALKING_RUNNING, + HealthDataType.TOTAL_CALORIES_BURNED + ]; + + Map> mappedData = {}; + + @override + FutureOr getHeartRate() async { + try { + final types = HealthDataType.HEART_RATE; + final endDate = DateTime.now(); + // final startDate = endDate.subtract(Duration(days: 365)); + final startDate = endDate.subtract(Duration(days: 365)); + final data = await getHeartData(startDate, endDate, types); + addDataToMap("heartRate",data ); + } catch (e) { + print('Error getting heart rate: $e'); + } + } + + @override + FutureOr getSleep() async { + try { + final types = HealthDataType.SLEEP_IN_BED; + final endDate = DateTime.now(); + final startDate = endDate.subtract(Duration(days: 365)); + final data = await getData(startDate, endDate, types); + addDataToMap("sleep",data ); + } catch (e) { + print('Error getting sleep data: $e'); + } + } + + @override + FutureOr getSteps() async { + try { + final types = HealthDataType.STEPS; + final endDate = DateTime.now(); + final startDate = endDate.subtract(Duration(days: 365)); + final data = await getData(startDate, endDate, types); + addDataToMap("steps",data ); + debugPrint('Steps Data: $data'); + } catch (e) { + debugPrint('Error getting steps: $e'); + } + } + + @override + Future getActivity() async { + try { + final types = HealthDataType.ACTIVE_ENERGY_BURNED; + final endDate = DateTime.now(); + final startDate = endDate.subtract(Duration(days: 365)); + final data = await getData(startDate, endDate, types); + addDataToMap("activity",data ); + debugPrint('Activity Data: $data'); + } catch (e) { + debugPrint('Error getting activity: $e'); + } + } + + @override + Future retrieveData() async { + return Result.value(getMappedData()); + } + + @override + Future getBloodOxygen() async { + try { + final types = HealthDataType.BLOOD_OXYGEN; + final endDate = DateTime.now(); + final startDate = endDate.subtract(Duration(days: 365)); + final data = await getData(startDate, endDate, types); + addDataToMapBloodOxygen("bloodOxygen", data); + } catch (e) { + debugPrint('Error getting blood oxygen: $e'); + } + } + + @override + Future getBodyTemperature() async { + try { + final types = HealthDataType.BODY_TEMPERATURE; + final endDate = DateTime.now(); + final startDate = endDate.subtract(Duration(days: 365)); + final data = await getData(startDate, endDate, types); + addDataToMap("bodyTemperature",data ); + } catch (e) { + debugPrint('Error getting body temp erature: $e'); + } + } + + @override + FutureOr getDistance() async { + try { + final types = HealthDataType.DISTANCE_WALKING_RUNNING; + final endDate = DateTime.now(); + final startDate = endDate.subtract(Duration(days: 365)); + final data = await getData(startDate, endDate, types); + addDataToMap("distance",data ); + } catch (e) { + debugPrint('Error getting distance: $e'); + } + } + + @override + Future> initDevice() async { + try { + final types = _healthPermissions; + final granted = await health.requestAuthorization(types); + await Permission.activityRecognition.request(); + await Permission.location.request(); + await Health().requestHealthDataHistoryAuthorization(); + return Result.value(granted); + } catch (e) { + debugPrint('Authorization error: $e'); + return Result.error(false); + } + } + + getData(startTime, endTime,type) async { + return await health.getHealthIntervalDataFromTypes( + startDate: startTime, + endDate: endTime, + types: [type], + interval: 3600, + ); + } + + void addDataToMap(String s, data) { + mappedData[s] = []; + for (var point in data) { + if (point.value is NumericHealthValue) { + final numericValue = (point.value as NumericHealthValue).numericValue; + Vitals vitals = Vitals( + value: (point.value as NumericHealthValue).numericValue.toStringAsFixed(2), + timestamp: point.dateFrom.toString() + ); + mappedData[s]?.add(vitals); + } + } + } + + void addDataToMapBloodOxygen(String s, data) { + mappedData[s] = []; + for (var point in data) { + if (point.value is NumericHealthValue) { + final numericValue = (point.value as NumericHealthValue).numericValue; + point.value = NumericHealthValue( + numericValue: numericValue * 100, + ); + Vitals vitals = Vitals(value: (point.value as NumericHealthValue).numericValue.toStringAsFixed(2), timestamp: point.dateFrom.toString()); + mappedData[s]?.add(vitals); + } + } + } + + getMappedData() { + return " { \"heartRate\": ${mappedData["heartRate"] ?? []}, \"sleep\": ${mappedData["sleep"] ?? []}, \"steps\": ${mappedData["steps"] ?? []}, \"activity\": ${mappedData["activity"] ?? []}, \"bloodOxygen\": ${mappedData["bloodOxygen"] ?? []}, \"bodyTemperature\": ${mappedData["bodyTemperature"] ?? []}, \"distance\": ${mappedData["distance"] ?? []} }"; + } + + getHeartData(DateTime startDate, DateTime endDate, HealthDataType types) async { + return await health.getHealthDataFromTypes( + startTime: startDate, + endTime: endDate, + types: [types], + ); + } +} diff --git a/lib/features/smartwatch_health_data/watch_connectors/huawei_watch_connecter.dart b/lib/features/smartwatch_health_data/watch_connectors/huawei_watch_connecter.dart new file mode 100644 index 00000000..a8300e2d --- /dev/null +++ b/lib/features/smartwatch_health_data/watch_connectors/huawei_watch_connecter.dart @@ -0,0 +1,86 @@ +import 'dart:async'; + +import 'package:async/src/result/result.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/services.dart'; +import 'package:hmg_patient_app_new/features/smartwatch_health_data/watch_connectors/watch_helper.dart'; +import 'package:huawei_health/huawei_health.dart'; + +class HuaweiHealthDataConnector extends WatchHelper{ + final MethodChannel _channel = MethodChannel('huawei_watch'); + @override + Future> initDevice() async{ + try{ + await _channel.invokeMethod('init'); + + }catch(e){ + + } + // List of scopes to ask for authorization. + // Note: These scopes should also be authorized on the Huawei Developer Console. + List scopes = [ + Scope.HEALTHKIT_STEP_READ, Scope.HEALTHKIT_OXYGEN_SATURATION_READ, // View and store height and weight data in Health Service Kit. + Scope.HEALTHKIT_HEARTRATE_READ, Scope.HEALTHKIT_SLEEP_READ, + Scope.HEALTHKIT_BODYTEMPERATURE_READ, Scope.HEALTHKIT_CALORIES_READ + ]; + try { + bool? result = await SettingController.getHealthAppAuthorization(); + debugPrint( + 'Granted Scopes for result == is $result}', + ); + return Result.value(true); + } catch (e) { + debugPrint('Error on authorization, Error:${e.toString()}'); + return Result.error(false); + } + } + + @override + Future getActivity() async { + DataType dataTypeResult = await SettingController.readDataType( + DataType.DT_CONTINUOUS_STEPS_DELTA.name + ); + + + } + + @override + Future getBloodOxygen() { + throw UnimplementedError(); + + } + + @override + Future getBodyTemperature() { + + throw UnimplementedError(); + } + + @override + FutureOr getHeartRate() { + throw UnimplementedError(); + } + + @override + FutureOr getSleep() { + throw UnimplementedError(); + } + + @override + FutureOr getSteps() { + throw UnimplementedError(); + } + + + @override + Future retrieveData() { + throw UnimplementedError(); + } + + @override + FutureOr getDistance() { + // TODO: implement getDistance + throw UnimplementedError(); + } + +} \ No newline at end of file diff --git a/lib/features/smartwatch_health_data/watch_connectors/samsung_health.dart b/lib/features/smartwatch_health_data/watch_connectors/samsung_health.dart new file mode 100644 index 00000000..d3cbcfc9 --- /dev/null +++ b/lib/features/smartwatch_health_data/watch_connectors/samsung_health.dart @@ -0,0 +1,98 @@ +import 'dart:async'; + +import 'package:async/src/result/result.dart'; +import 'package:hmg_patient_app_new/features/smartwatch_health_data/platform_channel/samsung_platform_channel.dart'; +import 'package:hmg_patient_app_new/features/smartwatch_health_data/watch_connectors/watch_helper.dart' show WatchHelper; + +class SamsungHealth extends WatchHelper { + + final SamsungPlatformChannel platformChannel = SamsungPlatformChannel(); + + @override + FutureOr getHeartRate() async { + try { + await platformChannel.getHeartRate(); + + }catch(e){ + print('Error getting heart rate: $e'); + } + + + } + + @override + Future> initDevice() async { + var result = await platformChannel.initDevice(); + if(result.isError){ + return result; + } + return await platformChannel.getRequestedPermission(); + } + + @override + FutureOr getSleep() async { + try { + await platformChannel.getSleep(); + + }catch(e){ + print('Error getting heart rate: $e'); + } + } + + @override + FutureOr getSteps() async{ + try { + await platformChannel.getSteps(); + + }catch(e){ + print('Error getting heart rate: $e'); + } + } + @override + Future getActivity() async{ + try { + await platformChannel.getActivity(); + }catch(e){ + print('Error getting heart rate: $e'); + } + } + + @override + Future retrieveData() async{ + try { + return await platformChannel.retrieveData(); + }catch(e){ + print('Error getting heart rate: $e'); + } + } + + @override + Future getBloodOxygen() async{ + try { + return await platformChannel.getBloodOxygen(); + }catch(e){ + print('Error getting heart rate: $e'); + } + } + + @override + Future getBodyTemperature() async { + try { + return await platformChannel.getBodyTemperature(); + }catch(e){ + print('Error getting heart rate: $e'); + } + } + + @override + FutureOr getDistance() async{ + try { + return await platformChannel.getDistance(); + }catch(e){ + print('Error getting heart rate: $e'); + } + } + + + +} \ No newline at end of file diff --git a/lib/features/smartwatch_health_data/watch_connectors/watch_helper.dart b/lib/features/smartwatch_health_data/watch_connectors/watch_helper.dart new file mode 100644 index 00000000..a09064d8 --- /dev/null +++ b/lib/features/smartwatch_health_data/watch_connectors/watch_helper.dart @@ -0,0 +1,14 @@ +import 'dart:async'; +import 'package:async/async.dart'; +abstract class WatchHelper { + Future> initDevice(); + FutureOr getHeartRate(); + FutureOr getSleep(); + FutureOr getSteps(); + FutureOr getDistance(); + Future getActivity(); + Future retrieveData(); + Future getBodyTemperature(); + Future getBloodOxygen(); + +} \ No newline at end of file diff --git a/lib/generated/locale_keys.g.dart b/lib/generated/locale_keys.g.dart index a28652d9..e9f60f5c 100644 --- a/lib/generated/locale_keys.g.dart +++ b/lib/generated/locale_keys.g.dart @@ -7,6 +7,8 @@ abstract class LocaleKeys { static const arabic = 'arabic'; static const login = 'login'; static const noDataAvailable = 'noDataAvailable'; + static const noRatingAvailable = 'noRatingAvailable'; + static const doctorDoesNotHaveRating = 'doctorDoesNotHaveRating'; static const confirm = 'confirm'; static const loadingText = 'loadingText'; static const kilometerUnit = 'kilometerUnit'; @@ -906,6 +908,7 @@ abstract class LocaleKeys { static const vat15 = 'vat15'; static const liveCare = 'liveCare'; static const recentVisits = 'recentVisits'; + static const favouriteDoctors = 'favouriteDoctors'; static const searchByClinic = 'searchByClinic'; static const tapToSelectClinic = 'tapToSelectClinic'; static const searchByDoctor = 'searchByDoctor'; @@ -1579,9 +1582,24 @@ abstract class LocaleKeys { static const invalidEligibility = 'invalidEligibility'; static const invalidInsurance = 'invalidInsurance'; static const continueCash = 'continueCash'; + static const applewatch = 'applewatch'; + static const applehealthapplicationshouldbeinstalledinyourphone = 'applehealthapplicationshouldbeinstalledinyourphone'; + static const unabletodetectapplicationinstalledpleasecomebackonceinstalled = 'unabletodetectapplicationinstalledpleasecomebackonceinstalled'; + static const applewatchshouldbeconnected = 'applewatchshouldbeconnected'; + static const samsungwatch = 'samsungwatch'; + static const samsunghealthapplicationshouldbeinstalledinyourphone = 'samsunghealthapplicationshouldbeinstalledinyourphone'; + static const samsungwatchshouldbeconnected = 'samsungwatchshouldbeconnected'; + static const huaweiwatch = 'huaweiwatch'; + static const huaweihealthapplicationshouldbeinstalledinyourphone = 'huaweihealthapplicationshouldbeinstalledinyourphone'; + static const huaweiwatchshouldbeconnected = 'huaweiwatchshouldbeconnected'; + static const whoopwatch = 'whoopwatch'; + static const whoophealthapplicationshouldbeinstalledinyourphone = 'whoophealthapplicationshouldbeinstalledinyourphone'; + static const whoopwatchshouldbeconnected = 'whoopwatchshouldbeconnected'; + static const updatetheinformation = 'updatetheinformation'; static const timeFor = 'timeFor'; static const hmgPolicies = 'hmgPolicies'; static const darkMode = 'darkMode'; + static const featureComingSoonDescription = 'featureComingSoonDescription'; static const generateAiAnalysisResult = 'generateAiAnalysisResult'; static const ratings = 'ratings'; static const hmgPharmacyText = 'hmgPharmacyText'; @@ -1590,6 +1608,43 @@ abstract class LocaleKeys { static const verifyInsurance = 'verifyInsurance'; static const tests = 'tests'; static const calendarPermissionAlert = 'calendarPermissionAlert'; + static const sortByNearestLocation = 'sortByNearestLocation'; + static const giveLocationPermissionForNearestList = 'giveLocationPermissionForNearestList'; static const sortByLocation = 'sortByLocation'; + static const timeForFirstReminder = 'timeForFirstReminder'; + static const reminderRemovalNote = 'reminderRemovalNote'; + static const communicationLanguage = 'communicationLanguage'; + static const cmcServiceHeader = 'cmcServiceHeader'; + static const cmcServiceDescription = 'cmcServiceDescription'; + static const eReferralServiceHeader = 'eReferralServiceHeader'; + static const eReferralServiceDescription = 'eReferralServiceDescription'; + static const bloodDonationServiceHeader = 'bloodDonationServiceHeader'; + static const bloodDonationServiceDescription = 'bloodDonationServiceDescription'; + static const healthTrackersServiceHeader = 'healthTrackersServiceHeader'; + static const healthTrackersServiceDescription = 'healthTrackersServiceDescription'; + static const waterConsumptionServiceHeader = 'waterConsumptionServiceHeader'; + static const waterConsumptionServiceDescription = 'waterConsumptionServiceDescription'; + static const smartWatchServiceHeader = 'smartWatchServiceHeader'; + static const smartWatchServiceDescription = 'smartWatchServiceDescription'; + static const liveChatServiceHeader = 'liveChatServiceHeader'; + static const liveChatServiceDescription = 'liveChatServiceDescription'; + static const emergencyServiceHeader = 'emergencyServiceHeader'; + static const emergencyServiceDescription = 'emergencyServiceDescription'; + static const homeHealthCareServiceHeader = 'homeHealthCareServiceHeader'; + static const homeHealthCareServiceDescription = 'homeHealthCareServiceDescription'; + static const profileOnlyText = 'profileOnlyText'; + static const information = 'information'; + static const noFavouriteDoctors = 'noFavouriteDoctors'; + static const addDoctors = 'addDoctors'; + static const favouriteList = 'favouriteList'; + static const later = 'later'; + static const cancelAppointmentConfirmMessage = 'cancelAppointmentConfirmMessage'; + static const acknowledged = 'acknowledged'; + static const searchLabResults = 'searchLabResults'; + static const callForAssistance = 'callForAssistance'; + static const oneWaySubtitle = 'oneWaySubtitle'; + static const twoWaySubtitle = 'twoWaySubtitle'; + static const toHospitalSubtitle = 'toHospitalSubtitle'; + static const fromHospitalSubtitle = 'fromHospitalSubtitle'; } diff --git a/lib/presentation/appointments/appointment_details_page.dart b/lib/presentation/appointments/appointment_details_page.dart index 072cb04a..dde08085 100644 --- a/lib/presentation/appointments/appointment_details_page.dart +++ b/lib/presentation/appointments/appointment_details_page.dart @@ -36,7 +36,9 @@ import 'package:hmg_patient_app_new/presentation/ask_doctor/ask_doctor_page.dart import 'package:hmg_patient_app_new/presentation/ask_doctor/doctor_response_page.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/widgets/appointment_calendar.dart'; import 'package:hmg_patient_app_new/presentation/contact_us/feedback_page.dart'; +import 'package:hmg_patient_app_new/presentation/home/navigation_screen.dart'; import 'package:hmg_patient_app_new/presentation/prescriptions/prescription_detail_page.dart'; +import 'package:hmg_patient_app_new/services/dialog_service.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; @@ -73,10 +75,10 @@ class _AppointmentDetailsPageState extends State { void initState() { scheduleMicrotask(() async { if (!AppointmentType.isArrived(widget.patientAppointmentHistoryResponseModel)) { - CalenderUtilsNew calendarUtils = await CalenderUtilsNew.instance; - var doesExist = await calendarUtils.checkIfEventExist("${widget.patientAppointmentHistoryResponseModel.appointmentNo}"); - myAppointmentsViewModel.setAppointmentReminder(doesExist, widget.patientAppointmentHistoryResponseModel); - setState(() {}); + // CalenderUtilsNew calendarUtils = await CalenderUtilsNew.instance; + // var doesExist = await calendarUtils.checkIfEventExist("${widget.patientAppointmentHistoryResponseModel.appointmentNo}"); + // myAppointmentsViewModel.setAppointmentReminder(doesExist, widget.patientAppointmentHistoryResponseModel); + // setState(() {}); } }); @@ -148,41 +150,57 @@ class _AppointmentDetailsPageState extends State { }); }, onCancelTap: () async { - LoaderBottomSheet.showLoader(loadingText: LocaleKeys.cancellingAppointmentPleaseWait.tr(context: context)); - await myAppointmentsViewModel.cancelAppointment( - patientAppointmentHistoryResponseModel: widget.patientAppointmentHistoryResponseModel, - onSuccess: (apiResponse) { - LoaderBottomSheet.hideLoader(); - myAppointmentsViewModel.setIsAppointmentDataToBeLoaded(true); - myAppointmentsViewModel.getPatientAppointments(true, false); - showCommonBottomSheetWithoutHeight( - context, - child: Utils.getSuccessWidget(loadingText: LocaleKeys.appointmentCancelledSuccessfully.tr(context: context)), - callBackFunc: () { - Navigator.of(context).pop(); - }, - title: "", - isCloseButtonVisible: true, - isDismissible: false, - isFullScreen: false, - ); - }, - onError: (err) { - LoaderBottomSheet.hideLoader(); - showCommonBottomSheetWithoutHeight( - context, - child: Utils.getErrorWidget(loadingText: err.toString()), - callBackFunc: () {}, - title: "", - isCloseButtonVisible: true, - isDismissible: false, - isFullScreen: false, - ); - }); - var isEventAddedOrRemoved = await CalenderUtilsNew.instance.checkAndRemove( id:"${widget.patientAppointmentHistoryResponseModel.appointmentNo}", ); - setState(() { - myAppointmentsViewModel.setAppointmentReminder(isEventAddedOrRemoved, widget.patientAppointmentHistoryResponseModel); - }); + showCommonBottomSheetWithoutHeight( + title: LocaleKeys.notice.tr(context: context), + context, + child: Utils.getWarningWidget( + loadingText: LocaleKeys.cancelAppointmentConfirmMessage.tr(context: context), + isShowActionButtons: true, + onCancelTap: () { + Navigator.of(context).pop(); + }, + onConfirmTap: () async { + Navigator.of(context).pop(); + LoaderBottomSheet.showLoader(loadingText: LocaleKeys.cancellingAppointmentPleaseWait.tr(context: context)); + myAppointmentsViewModel.onTabChange(0); + myAppointmentsViewModel.updateListWRTTab(0); + await myAppointmentsViewModel.cancelAppointment( + patientAppointmentHistoryResponseModel: widget.patientAppointmentHistoryResponseModel, + onSuccess: (apiResponse) async { + LoaderBottomSheet.hideLoader(); + myAppointmentsViewModel.setIsAppointmentDataToBeLoaded(true); + myAppointmentsViewModel.getPatientAppointments(true, false); + showCommonBottomSheet( + context, + child: Utils.getSuccessWidget(loadingText: LocaleKeys.appointmentCancelledSuccessfully.tr(context: context)), + callBackFunc: (str) { + Navigator.of(context).pop(); + }, + title: "", + isCloseButtonVisible: false, + isDismissible: false, + isFullScreen: false, + isAutoDismiss: true, + height: ResponsiveExtension.screenHeight * 0.3, + ); + }, + onError: (err) { + LoaderBottomSheet.hideLoader(); + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: err.toString()), + callBackFunc: () {}, + title: "", isCloseButtonVisible: true, isDismissible: false, isFullScreen: false); + }); + }), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + // var isEventAddedOrRemoved = await CalenderUtilsNew.instance.checkAndRemove( id:"${widget.patientAppointmentHistoryResponseModel.appointmentNo}", ); + // setState(() { + // myAppointmentsViewModel.setAppointmentReminder(isEventAddedOrRemoved, widget.patientAppointmentHistoryResponseModel); + // }); }, onRescheduleTap: () async { openDoctorScheduleCalendar(); @@ -229,16 +247,22 @@ class _AppointmentDetailsPageState extends State { LoaderBottomSheet.hideLoader(); myAppointmentsViewModel.setIsAppointmentDataToBeLoaded(true); myAppointmentsViewModel.initAppointmentsViewModel(); - myAppointmentsViewModel.getPatientAppointments(true, false); + // myAppointmentsViewModel.getPatientAppointments(true, false); showCommonBottomSheetWithoutHeight( title: "", context, child: Utils.getSuccessWidget(loadingText: LocaleKeys.appointmentConfirmedSuccessfully.tr(context: context)), callBackFunc: () { - Navigator.of(context).pop(); + Navigator.pushAndRemoveUntil( + context, + CustomPageRoute( + page: LandingNavigation(), + ), + (r) => false); }, isFullScreen: false, - isCloseButtonVisible: true, + isCloseButtonVisible: false, + isAutoDismiss: true ); }); }, @@ -286,7 +310,7 @@ class _AppointmentDetailsPageState extends State { Positioned( bottom: 0, child: SizedBox( - width: MediaQuery.of(context).size.width * 0.785, + width: MediaQuery.of(context).size.width - 85.w, child: CustomButton( onPressed: () async { if (widget.patientAppointmentHistoryResponseModel.projectID == 130 || widget.patientAppointmentHistoryResponseModel.projectID == 120) { @@ -394,7 +418,6 @@ class _AppointmentDetailsPageState extends State { }, isMultiAllowed: true, onMultiDateSuccess: (int selectedIndex) async { - isEventAddedOrRemoved = await calender.createOrUpdateEvent( title: "Appointment Reminder with ${widget.patientAppointmentHistoryResponseModel.doctorNameObj} on ${DateUtil.convertStringToDate(widget.patientAppointmentHistoryResponseModel.appointmentDate)}, Appointment #${widget.patientAppointmentHistoryResponseModel.appointmentNo}", @@ -822,8 +845,7 @@ class _AppointmentDetailsPageState extends State { child: LocaleKeys.upcomingPaymentNow .tr(context: context) .toText12(fontWeight: FontWeight.w500, color: AppColors.greyTextColor)), - "VAT 15%(${widget.patientAppointmentHistoryResponseModel.patientTaxAmount})" - .toText14(isBold: true, color: AppColors.greyTextColor, letterSpacing: -2), + "VAT 15%(${widget.patientAppointmentHistoryResponseModel.patientTaxAmount})".toText14(isBold: true, color: AppColors.greyTextColor, letterSpacing: -0.64), ], ), SizedBox(height: 18.h), @@ -831,7 +853,7 @@ class _AppointmentDetailsPageState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ SizedBox( - width: 150.h, + width: 200.h, child: Utils.getPaymentMethods(), ), Row( @@ -989,12 +1011,13 @@ class _AppointmentDetailsPageState extends State { doctorImageURL: widget.patientAppointmentHistoryResponseModel.doctorImageURL, doctorTitle: widget.patientAppointmentHistoryResponseModel.doctorTitle, name: widget.patientAppointmentHistoryResponseModel.doctorNameObj, - nationalityFlagURL: "https://hmgwebservices.com/Images/flag/SYR.png", + nationalityFlagURL: "", speciality: [], clinicName: widget.patientAppointmentHistoryResponseModel.clinicName, projectName: widget.patientAppointmentHistoryResponseModel.projectName, ); bookAppointmentsViewModel.setSelectedDoctor(doctor); + bookAppointmentsViewModel.setIsPatientRescheduleAppointment(true); LoaderBottomSheet.showLoader(loadingText: LocaleKeys.fetchingDoctorSchedulePleaseWait.tr(context: context)); await bookAppointmentsViewModel.getDoctorFreeSlots( isBookingForLiveCare: false, @@ -1038,17 +1061,19 @@ class _AppointmentDetailsPageState extends State { myAppointmentsViewModel.initAppointmentsViewModel(); myAppointmentsViewModel.getPatientAppointments(true, false); Navigator.of(context).pop(); - }, - title: "", - height: ResponsiveExtension.screenHeight * 0.3, - isCloseButtonVisible: true, - isDismissible: false, - isFullScreen: false, + }, title: "", height: ResponsiveExtension.screenHeight * 0.3, isAutoDismiss: true, isCloseButtonVisible: true, isDismissible: false, isFullScreen: false, isSuccessDialog: true); }); // LoaderBottomSheet.hideLoader(); case 15: - break; + getIt.get().showCommonBottomSheetWithoutH( + message: LocaleKeys.upcomingPaymentPending.tr(context: context), + label: LocaleKeys.notice.tr(), + onOkPressed: () {}, + okLabel: "confirm", + cancelLabel: LocaleKeys.acknowledged.tr(context: context), + isConfirmButton: true, + ); case 20: myAppointmentsViewModel.setIsPatientAppointmentShareLoading(true); Navigator.of(context).push( diff --git a/lib/presentation/appointments/appointment_payment_page.dart b/lib/presentation/appointments/appointment_payment_page.dart index d762f795..e755cfeb 100644 --- a/lib/presentation/appointments/appointment_payment_page.dart +++ b/lib/presentation/appointments/appointment_payment_page.dart @@ -258,7 +258,8 @@ class _AppointmentPaymentPageState extends State { CustomPageRoute( page: InsuranceHomePage(), ), - ); + ) + .then((val) {}); }, backgroundColor: AppColors.primaryRedColor, borderColor: AppColors.secondaryLightRedBorderColor, diff --git a/lib/presentation/appointments/my_appointments_page.dart b/lib/presentation/appointments/my_appointments_page.dart index c9e70487..71bf30bf 100644 --- a/lib/presentation/appointments/my_appointments_page.dart +++ b/lib/presentation/appointments/my_appointments_page.dart @@ -55,36 +55,40 @@ class _MyAppointmentsPageState extends State { bookAppointmentsViewModel = Provider.of(context, listen: false); return Scaffold( backgroundColor: AppColors.bgScaffoldColor, - body: CollapsingListView( - title: LocaleKeys.appointmentsList.tr(context: context), - child: SingleChildScrollView( - child: Column( - children: [ - SizedBox(height: 16.h), - CustomTabBar( - activeTextColor: Color(0xffED1C2B), - activeBackgroundColor: Color(0xffED1C2B).withValues(alpha: .1), - tabs: [ - CustomTabBarModel(null, LocaleKeys.allAppt.tr(context: context)), - CustomTabBarModel(null, LocaleKeys.upcoming.tr(context: context)), - CustomTabBarModel(null, LocaleKeys.completed.tr(context: context)), - ], - onTabChange: (index) { - setState(() { - expandedIndex = null; - }); - myAppointmentsViewModel.onTabChange(index); - myAppointmentsViewModel.updateListWRTTab(index); - context.read().flush(); - }, - ).paddingSymmetrical(24.h, 0.h), - Consumer(builder: (context, myAppointmentsVM, child) { - return getSelectedTabData(myAppointmentsVM.selectedTabIndex, myAppointmentsVM); - }), - ], + body: Consumer(builder: (context, myAppointmentsVM, child) { + return CollapsingListView( + title: LocaleKeys.appointmentsList.tr(context: context), + child: SingleChildScrollView( + child: Column( + children: [ + SizedBox(height: 16.h), + CustomTabBar( + initialIndex: myAppointmentsVM.selectedTabIndex, + activeTextColor: Color(0xffED1C2B), + activeBackgroundColor: Color(0xffED1C2B).withValues(alpha: .1), + tabs: [ + CustomTabBarModel(null, LocaleKeys.allAppt.tr(context: context)), + CustomTabBarModel(null, LocaleKeys.upcoming.tr(context: context)), + CustomTabBarModel(null, LocaleKeys.completed.tr(context: context)), + ], + onTabChange: (index) { + setState(() { + expandedIndex = null; + }); + myAppointmentsViewModel.onTabChange(index); + myAppointmentsViewModel.updateListWRTTab(index); + context.read().flush(); + }, + ).paddingSymmetrical(24.h, 0.h), + // Consumer(builder: (context, myAppointmentsVM, child) { + // return + getSelectedTabData(myAppointmentsVM.selectedTabIndex, myAppointmentsVM), + // }), + ], ), - ), - ), + ), + ); + }), ); } @@ -144,7 +148,7 @@ class _MyAppointmentsPageState extends State { ? myAppointmentsVM.patientAppointmentsViewList.length : 1, itemBuilder: (context, index) { - final isExpanded = expandedIndex == index; + final isExpanded = myAppointmentsVM.selectedTabIndex == 1 ? true : expandedIndex == index; return myAppointmentsVM.isMyAppointmentsLoading ? Container( decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.h, hasShadow: true), diff --git a/lib/presentation/appointments/my_doctors_page.dart b/lib/presentation/appointments/my_doctors_page.dart index 002f537c..26e5927b 100644 --- a/lib/presentation/appointments/my_doctors_page.dart +++ b/lib/presentation/appointments/my_doctors_page.dart @@ -218,8 +218,17 @@ class _MyDoctorsPageState extends State { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - AppCustomChipWidget(labelText: "${group.length} ${'doctors'}"), - Icon(isExpanded ? Icons.expand_less : Icons.expand_more), + AppCustomChipWidget( + richText: Row( + children: [ + "${group.length} ".toText10(isEnglishOnly: true), + "${LocaleKeys.doctors.tr(context: context)} ".toText10() + ], + ), + // labelText: "${group.length} ${'doctors'}", + isEnglishOnly: true, + ), + Icon(isExpanded ? Icons.expand_less : Icons.expand_more), ], ), SizedBox(height: 8.h), @@ -289,10 +298,10 @@ class _MyDoctorsPageState extends State { flex: 2, child: CustomButton( icon: AppAssets.view_report_icon, - iconColor: AppColors.primaryRedColor, + iconColor: doctor?.isActiveDoctorProfile == false ? AppColors.inputLabelTextColor.withValues(alpha: 1.0) : AppColors.primaryRedColor, iconSize: 16.h, text: LocaleKeys.viewProfile.tr(context: context), - onPressed: () async { + onPressed: doctor?.isActiveDoctorProfile == false ? () {} : () async { bookAppointmentsViewModel.setSelectedDoctor(DoctorsListResponseModel( clinicID: doctor?.clinicID ?? 0, projectID: doctor?.projectID ?? 0, @@ -323,9 +332,9 @@ class _MyDoctorsPageState extends State { ); }); }, - backgroundColor: AppColors.secondaryLightRedColor, - borderColor: AppColors.secondaryLightRedColor, - textColor: AppColors.primaryRedColor, + backgroundColor: doctor?.isActiveDoctorProfile == false ? AppColors.inputLabelTextColor.withValues(alpha: 0.3) : AppColors.secondaryLightRedColor, + borderColor: doctor?.isActiveDoctorProfile == false ? AppColors.inputLabelTextColor.withValues(alpha: 0.01) : AppColors.secondaryLightRedColor, + textColor: doctor?.isActiveDoctorProfile == false ? AppColors.inputLabelTextColor.withValues(alpha: 1.0) : AppColors.primaryRedColor, fontSize: 14, fontWeight: FontWeight.w500, borderRadius: 12, diff --git a/lib/presentation/appointments/widgets/appointment_card.dart b/lib/presentation/appointments/widgets/appointment_card.dart index 52a1c219..679c4bac 100644 --- a/lib/presentation/appointments/widgets/appointment_card.dart +++ b/lib/presentation/appointments/widgets/appointment_card.dart @@ -148,8 +148,8 @@ class AppointmentCard extends StatelessWidget { Utils.buildSvgWithAssets(icon: AppAssets.rating_icon, width: 15.w, height: 15.h, iconColor: AppColors.ratingColorYellow), SizedBox(height: 2.h), (isFoldable || isTablet) - ? "${patientAppointmentHistoryResponseModel.decimalDoctorRate}".toText9(isBold: true, color: AppColors.textColor) - : "${patientAppointmentHistoryResponseModel.decimalDoctorRate ?? "0.0"}".toText11(isBold: true, color: AppColors.textColor), + ? "${patientAppointmentHistoryResponseModel.decimalDoctorRate}".toText9(isBold: true, color: AppColors.textColor, isEnglishOnly: true) + : "${patientAppointmentHistoryResponseModel.decimalDoctorRate ?? "0.0"}".toText11(isBold: true, color: AppColors.textColor, isEnglishOnly: true), ], ), ).circle(100).toShimmer2(isShow: isLoading), @@ -165,7 +165,16 @@ class AppointmentCard extends StatelessWidget { children: [ (isLoading ? 'Dr' : "${patientAppointmentHistoryResponseModel.doctorTitle}").toText16(isBold: true, maxlines: 1), (isLoading ? 'John Doe' : " ${patientAppointmentHistoryResponseModel.doctorNameObj!.truncate(20)}") - .toText16(isBold: true, maxlines: 1, isEnglishOnly: !Utils.isArabicText(patientAppointmentHistoryResponseModel.doctorNameObj ?? "John Doe")) + .toText16(isBold: true, maxlines: 1, isEnglishOnly: !Utils.isArabicText(patientAppointmentHistoryResponseModel.doctorNameObj ?? "John Doe")), + SizedBox(width: 12.w), + (patientAppointmentHistoryResponseModel.doctorNationalityFlagURL != null && patientAppointmentHistoryResponseModel.doctorNationalityFlagURL!.isNotEmpty) + ? Image.network( + patientAppointmentHistoryResponseModel.doctorNationalityFlagURL ?? "https://hmgwebservices.com/Images/flag/SAU.png", + width: 20.h, + height: 15.h, + fit: BoxFit.cover, + ) + : SizedBox.shrink(), ], ).toShimmer2(isShow: isLoading), SizedBox(height: 8.h), @@ -456,7 +465,7 @@ class AppointmentCard extends StatelessWidget { doctorImageURL: patientAppointmentHistoryResponseModel.doctorImageURL, doctorTitle: patientAppointmentHistoryResponseModel.doctorTitle, name: patientAppointmentHistoryResponseModel.doctorNameObj, - nationalityFlagURL: 'https://hmgwebservices.com/Images/flag/SYR.png', + nationalityFlagURL: '', speciality: [], clinicName: patientAppointmentHistoryResponseModel.clinicName, projectName: patientAppointmentHistoryResponseModel.projectName, diff --git a/lib/presentation/appointments/widgets/appointment_checkin_bottom_sheet.dart b/lib/presentation/appointments/widgets/appointment_checkin_bottom_sheet.dart index 95fc09b8..2acc076e 100644 --- a/lib/presentation/appointments/widgets/appointment_checkin_bottom_sheet.dart +++ b/lib/presentation/appointments/widgets/appointment_checkin_bottom_sheet.dart @@ -71,20 +71,20 @@ class AppointmentCheckinBottomSheet extends StatelessWidget { } }); }), - SizedBox(height: 16.h), - checkInOptionCard( - AppAssets.checkin_nfc_icon, - LocaleKeys.nfcNearFieldCommunication.tr(context: context), - LocaleKeys.scanPhoneViaNFC.tr(context: context), - ).onPress(() { - Future.delayed(const Duration(milliseconds: 500), () { - showNfcReader(context, onNcfScan: (String nfcId) { - Future.delayed(const Duration(milliseconds: 100), () { - sendCheckInRequest(nfcId, 1, context); - }); - }, onCancel: () {}); - }); - }), + // SizedBox(height: 16.h), + // checkInOptionCard(O + // AppAssets.checkin_nfc_icon, + // LocaleKeys.nfcNearFieldCommunication.tr(context: context), + // LocaleKeys.scanPhoneViaNFC.tr(context: context), + // ).onPress(() { + // Future.delayed(const Duration(milliseconds: 500), () { + // showNfcReader(context, onNcfScan: (String nfcId) { + // Future.delayed(const Duration(milliseconds: 100), () { + // sendCheckInRequest(nfcId, 1, context); + // }); + // }, onCancel: () {}); + // }); + // }), SizedBox(height: 16.h), checkInOptionCard( AppAssets.checkin_qr_icon, diff --git a/lib/presentation/appointments/widgets/appointment_doctor_card.dart b/lib/presentation/appointments/widgets/appointment_doctor_card.dart index 5a47708e..f00284c0 100644 --- a/lib/presentation/appointments/widgets/appointment_doctor_card.dart +++ b/lib/presentation/appointments/widgets/appointment_doctor_card.dart @@ -1,19 +1,27 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/utils/date_util.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; +import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/doctors_list_response_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/patient_appointment_history_response_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/utils/appointment_type.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/presentation/book_appointment/doctor_profile_page.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; +import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; import 'dart:ui' as ui; +import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; +import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; + class AppointmentDoctorCard extends StatelessWidget { const AppointmentDoctorCard( {super.key, required this.patientAppointmentHistoryResponseModel, required this.onRescheduleTap, required this.onCancelTap, required this.onAskDoctorTap, this.renderWidgetForERDisplay = false}); @@ -66,7 +74,7 @@ class AppointmentDoctorCard extends StatelessWidget { children: [ Utils.buildSvgWithAssets(icon: AppAssets.rating_icon, width: 15.w, height: 15.h, iconColor: AppColors.ratingColorYellow), SizedBox(height: 2.h), - "${patientAppointmentHistoryResponseModel.decimalDoctorRate ?? 0.0}".toText11(isBold: true, color: AppColors.textColor), + "${patientAppointmentHistoryResponseModel.decimalDoctorRate ?? 0.0}".toText11(isBold: true, color: AppColors.textColor, isEnglishOnly: true), ], ), ).circle(100), @@ -75,10 +83,24 @@ class AppointmentDoctorCard extends StatelessWidget { ), SizedBox(width: 16.w), Expanded( + flex: 9, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - patientAppointmentHistoryResponseModel.doctorNameObj!.toText16(isBold: true, isEnglishOnly: !Utils.isArabicText(patientAppointmentHistoryResponseModel.doctorNameObj ?? "")), + Row( + children: [ + patientAppointmentHistoryResponseModel.doctorNameObj!.toText16(isBold: true, isEnglishOnly: !Utils.isArabicText(patientAppointmentHistoryResponseModel.doctorNameObj ?? "")), + SizedBox(width: 12.w), + (patientAppointmentHistoryResponseModel.doctorNationalityFlagURL != null && patientAppointmentHistoryResponseModel.doctorNationalityFlagURL!.isNotEmpty) + ? Image.network( + patientAppointmentHistoryResponseModel.doctorNationalityFlagURL ?? "https://hmgwebservices.com/Images/flag/SAU.png", + width: 20.h, + height: 15.h, + fit: BoxFit.cover, + ) + : SizedBox.shrink(), + ], + ), SizedBox(height: 8.h), Wrap( direction: Axis.horizontal, @@ -120,6 +142,39 @@ class AppointmentDoctorCard extends StatelessWidget { ], ), ), + Utils.buildSvgWithAssets(icon: AppAssets.doctor_profile_icon, width: 20.h, height: 20.h, fit: BoxFit.scaleDown).onPress(() async { + DoctorsListResponseModel selectedDoctor = DoctorsListResponseModel(); + selectedDoctor.doctorID = patientAppointmentHistoryResponseModel.doctorID; + selectedDoctor.doctorImageURL = patientAppointmentHistoryResponseModel.doctorImageURL; + selectedDoctor.name = patientAppointmentHistoryResponseModel.doctorNameObj; + selectedDoctor.doctorTitle = patientAppointmentHistoryResponseModel.doctorTitle; + selectedDoctor.nationalityFlagURL = ""; + selectedDoctor.speciality = patientAppointmentHistoryResponseModel.doctorSpeciality; + selectedDoctor.clinicName = patientAppointmentHistoryResponseModel.clinicName; + selectedDoctor.projectName = patientAppointmentHistoryResponseModel.projectName; + selectedDoctor.clinicID = patientAppointmentHistoryResponseModel.clinicID; + selectedDoctor.projectID = patientAppointmentHistoryResponseModel.projectID; + + getIt.get().setSelectedDoctor(selectedDoctor); + LoaderBottomSheet.showLoader(); + await getIt.get().getDoctorProfile(onSuccess: (dynamic respData) { + LoaderBottomSheet.hideLoader(); + Navigator.of(context).push( + CustomPageRoute( + page: DoctorProfilePage(), + ), + ); + }, onError: (err) { + LoaderBottomSheet.hideLoader(); + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: err), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + }); + }) ], ), SizedBox(height: 8.h), diff --git a/lib/presentation/appointments/widgets/hospital_bottom_sheet/hospital_bottom_sheet_body.dart b/lib/presentation/appointments/widgets/hospital_bottom_sheet/hospital_bottom_sheet_body.dart index f3301f0c..8904e585 100644 --- a/lib/presentation/appointments/widgets/hospital_bottom_sheet/hospital_bottom_sheet_body.dart +++ b/lib/presentation/appointments/widgets/hospital_bottom_sheet/hospital_bottom_sheet_body.dart @@ -1,11 +1,13 @@ import 'package:easy_localization/easy_localization.dart' show StringTranslateExtension; import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/enums.dart'; import 'package:hmg_patient_app_new/core/utils/debouncer.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart' show Utils; import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; -import 'package:hmg_patient_app_new/features/my_appointments/appointment_via_region_viewmodel.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/facility_selection.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/doctor_list_api_response.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; @@ -14,7 +16,6 @@ import 'package:hmg_patient_app_new/theme/colors.dart' show AppColors; import 'package:hmg_patient_app_new/widgets/input_widget.dart'; import 'package:provider/provider.dart'; -import '../../../../features/my_appointments/models/resp_models/hospital_model.dart' show HospitalsModel; import '../../../emergency_services/call_ambulance/widgets/type_selection_widget.dart' show TypeSelectionWidget; // class HospitalBottomSheetBody extends StatelessWidget { @@ -125,66 +126,111 @@ class HospitalBottomSheetBody extends StatelessWidget { final Function(FacilitySelection) onFacilityClicked; final Function(PatientDoctorAppointmentList) onHospitalClicked; final Function(String) onHospitalSearch; + final bool sortByLocation; + final Function(bool)? onSortByLocationToggle; - HospitalBottomSheetBody({super.key, required this.hmcCount, required this.hmgCount, this.displayList, required this.selectedFacility, required this.onFacilityClicked, required this.onHospitalClicked, required this.onHospitalSearch, required this.searchText}); + HospitalBottomSheetBody({super.key, required this.hmcCount, required this.hmgCount, this.displayList, required this.selectedFacility, required this.onFacilityClicked, required this.onHospitalClicked, required this.onHospitalSearch, required this.searchText, this.sortByLocation = false, this.onSortByLocationToggle}); @override Widget build(BuildContext context) { - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - TextInputWidget( - labelText: LocaleKeys.search.tr(context: context), - hintText: LocaleKeys.searchHospital.tr(context: context), - controller: searchText, - onChange: (value) { - debouncer.run((){ - onHospitalSearch(value??""); - }); - }, - isEnable: true, - prefix: null, - - autoFocus: false, - isBorderAllowed: false, - keyboardType: TextInputType.text, - isAllowLeadingIcon: true, - selectionType: SelectionTypeEnum.search, - padding: EdgeInsets.symmetric( - vertical: ResponsiveExtension(10).h, - horizontal: ResponsiveExtension(15).h, + return Consumer(builder: (context, myAppointmentsVM, child) { + if (myAppointmentsVM.isRegionListLoading) { + return Container( + height: MediaQuery.of(context).size.height * 0.3, + decoration: BoxDecoration( + color: AppColors.whiteColor, + borderRadius: BorderRadius.vertical(top: Radius.circular(16)), ), - ), - SizedBox(height: 24.h), - TypeSelectionWidget( - selectedFacility:selectedFacility , - hmcCount: hmcCount.toString(), - hmgCount: hmgCount.toString(), - onitemClicked: (selectedValue){ - onFacilityClicked(selectedValue); - }, - ), - SizedBox(height: 21.h), - SizedBox( - height: MediaQuery.sizeOf(context).height * .4, - child: ListView.separated( - itemBuilder: (_, index) - { - var hospital = displayList?[index]; - return HospitalListItem( - hospitalData: hospital, - isLocationEnabled: true, - ).onPress(() { - onHospitalClicked(hospital!); - });}, - separatorBuilder: (_, __) => SizedBox( - height: 16.h, + child: Center( + child: Utils.getLoadingWidget(), + ), + ); + } else { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextInputWidget( + labelText: LocaleKeys.search.tr(context: context), + hintText: LocaleKeys.searchHospital.tr(context: context), + controller: searchText, + onChange: (value) { + debouncer.run(() { + onHospitalSearch(value ?? ""); + }); + }, + isEnable: true, + prefix: null, + autoFocus: false, + isBorderAllowed: false, + keyboardType: TextInputType.text, + isAllowLeadingIcon: true, + selectionType: SelectionTypeEnum.search, + padding: EdgeInsets.symmetric( + vertical: ResponsiveExtension(10).h, + horizontal: ResponsiveExtension(15).h, + ), + ), + SizedBox(height: 16.h), + if (onSortByLocationToggle != null) + Padding( + padding: EdgeInsets.symmetric(horizontal: 4.w), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.location, iconColor: AppColors.greyTextColor, width: 18.h, height: 18.h), + SizedBox(width: 8.w), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LocaleKeys.sortByLocation.tr(context: context).toText14(isBold: true), + // SizedBox(height: 4.h), + LocaleKeys.sortByNearestLocation.tr(context: context).toText11(color: AppColors.textColorLight, weight: FontWeight.w500), + ], + ), + ], + ), + Switch( + value: sortByLocation, + onChanged: onSortByLocationToggle, + activeThumbColor: AppColors.successColor, + activeTrackColor: AppColors.successColor.withValues(alpha: 0.15), + ), + ], + ), ), - itemCount: displayList?.length ?? 0, - )) - ], - ); + if (onSortByLocationToggle != null) SizedBox(height: 8.h), + TypeSelectionWidget( + selectedFacility: selectedFacility, + hmcCount: hmcCount.toString(), + hmgCount: hmgCount.toString(), + onitemClicked: (selectedValue) { + onFacilityClicked(selectedValue); + }, + ), + SizedBox(height: 21.h), + SizedBox( + height: MediaQuery.sizeOf(context).height * .4, + child: ListView.separated( + itemBuilder: (_, index) { + var hospital = displayList?[index]; + return HospitalListItem( + hospitalData: hospital, + isLocationEnabled: sortByLocation, + ).onPress(() { + onHospitalClicked(hospital!); + }); + }, + separatorBuilder: (_, __) => SizedBox( + height: 16.h, + ), + itemCount: displayList?.length ?? 0, + )) + ], + ); + } + }); } } diff --git a/lib/presentation/appointments/widgets/hospital_bottom_sheet/hospital_list_items.dart b/lib/presentation/appointments/widgets/hospital_bottom_sheet/hospital_list_items.dart index b0a717b7..26b4526f 100644 --- a/lib/presentation/appointments/widgets/hospital_bottom_sheet/hospital_list_items.dart +++ b/lib/presentation/appointments/widgets/hospital_bottom_sheet/hospital_list_items.dart @@ -10,6 +10,8 @@ import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/ import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; +import 'dart:ui' as ui; + class HospitalListItem extends StatelessWidget { final PatientDoctorAppointmentList? hospitalData; final bool isLocationEnabled; @@ -76,12 +78,16 @@ class HospitalListItem extends StatelessWidget { children: [ Visibility( visible: (hospitalData?.distanceInKMs != "0"), - child: AppCustomChipWidget( - labelText: "${hospitalData?.distanceInKMs ?? ""} km", - deleteIcon: AppAssets.location_red, - deleteIconSize: Size(9, 12), - backgroundColor: AppColors.secondaryLightRedColor, - textColor: AppColors.errorColor, + child: Directionality( + textDirection: ui.TextDirection.ltr, + child: AppCustomChipWidget( + labelText: "${hospitalData?.distanceInKMs ?? ""} km", + deleteIcon: AppAssets.location_red, + deleteIconSize: Size(9, 12), + backgroundColor: AppColors.secondaryLightRedColor, + textColor: AppColors.errorColor, + isEnglishOnly: true, + ), ), ), Visibility( @@ -97,14 +103,14 @@ class HospitalListItem extends StatelessWidget { // ) ], )), - Visibility( - visible: !isLocationEnabled, - child: AppCustomChipWidget( - labelText: "Location turned off", - deleteIcon: AppAssets.location_unavailable, - deleteIconSize: Size(9.w, 12.h), - textColor: AppColors.blackColor, - )), + // Visibility( + // visible: !isLocationEnabled, + // child: AppCustomChipWidget( + // labelText: "Location turned off", + // deleteIcon: AppAssets.location_unavailable, + // deleteIconSize: Size(9.w, 12.h), + // textColor: AppColors.blackColor, + // )), ], ); } diff --git a/lib/presentation/appointments/widgets/region_bottomsheet/region_list_widget.dart b/lib/presentation/appointments/widgets/region_bottomsheet/region_list_widget.dart index 489ee8b7..ef80ec7b 100644 --- a/lib/presentation/appointments/widgets/region_bottomsheet/region_list_widget.dart +++ b/lib/presentation/appointments/widgets/region_bottomsheet/region_list_widget.dart @@ -3,6 +3,9 @@ import 'dart:async'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/location_util.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart' show Utils; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; @@ -14,6 +17,7 @@ import 'package:hmg_patient_app_new/presentation/appointments/widgets/region_bot import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:lottie/lottie.dart'; +import 'package:permission_handler/permission_handler.dart'; import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -33,6 +37,7 @@ class _RegionBottomSheetBodyState extends State { @override void initState() { scheduleMicrotask(() { + regionalViewModel.initSortByLocation(); if (regionalViewModel.regionBottomSheetType == RegionBottomSheetType.FOR_REGION || regionalViewModel.regionBottomSheetType == RegionBottomSheetType.REGION_FOR_DENTAL_AND_LASER ) { myAppointmentsViewModel.getRegionMappedProjectList(); } else if (regionalViewModel.regionBottomSheetType == RegionBottomSheetType.FOR_CLINIIC) { @@ -110,30 +115,136 @@ class _RegionBottomSheetBodyState extends State { } else { return SizedBox( height: MediaQuery.of(context).size.height * 0.5, - child: ListView.separated( - itemCount: myAppointmentsVM.hospitalList?.registeredDoctorMap?.length ?? 0, - separatorBuilder: (_, __) { - return SizedBox( - height: 16.h, - ); - }, - itemBuilder: (_, index) { - String key = myAppointmentsVM.hospitalList?.registeredDoctorMap?.keys.toList()[index] ?? ''; - return RegionListItem( - title: key, - subTitle: "", - hmcCount: "${myAppointmentsVM.hospitalList?.registeredDoctorMap?[key]?.hmcSize ?? 0}", - hmgCount: "${myAppointmentsVM.hospitalList?.registeredDoctorMap?[key]?.hmgSize ?? 0}", - ).onPress(() { - regionalViewModel.setSelectedRegionId(key); - regionalViewModel.setDisplayListAndRegionHospitalList(myAppointmentsVM.hospitalList?.registeredDoctorMap![key]); - regionalViewModel.setBottomSheetState(AppointmentViaRegionState.HOSPITAL_SELECTION); - }); - }, + child: Column( + children: [ + // Consumer( + // builder: (context, regionVM, _) { + // return Padding( + // padding: EdgeInsets.symmetric(horizontal: 4.w, vertical: 8.h), + // child: Row( + // mainAxisAlignment: MainAxisAlignment.spaceBetween, + // children: [ + // Row( + // children: [ + // Utils.buildSvgWithAssets(icon: AppAssets.location, iconColor: AppColors.greyTextColor, width: 24.h, height: 24.h), + // SizedBox(width: 12.w), + // Column( + // crossAxisAlignment: CrossAxisAlignment.start, + // children: [ + // LocaleKeys.sortByLocation.tr(context: context).toText14(isBold: true), + // // SizedBox(height: 4.h), + // LocaleKeys.sortByNearestLocation.tr(context: context).toText11(color: AppColors.textColorLight, weight: FontWeight.w500), + // ], + // ), + // ], + // ), + // Switch( + // value: regionVM.sortByLocation, + // onChanged: (value) => _handleSortByLocationToggle(value), + // activeThumbColor: AppColors.successColor, + // activeTrackColor: AppColors.successColor.withValues(alpha: 0.15), + // ), + // ], + // ), + // ); + // }, + // ), + Expanded( + child: ListView.separated( + itemCount: myAppointmentsVM.hospitalList?.registeredDoctorMap?.length ?? 0, + separatorBuilder: (_, __) { + return SizedBox( + height: 16.h, + ); + }, + itemBuilder: (_, index) { + String key = myAppointmentsVM.hospitalList?.registeredDoctorMap?.keys.toList()[index] ?? ''; + return RegionListItem( + title: key, + subTitle: "", + hmcCount: "${myAppointmentsVM.hospitalList?.registeredDoctorMap?[key]?.hmcSize ?? 0}", + hmgCount: "${myAppointmentsVM.hospitalList?.registeredDoctorMap?[key]?.hmgSize ?? 0}", + ).onPress(() { + regionalViewModel.setSelectedRegionId(key); + regionalViewModel.setDisplayListAndRegionHospitalList(myAppointmentsVM.hospitalList?.registeredDoctorMap![key]); + regionalViewModel.setBottomSheetState(AppointmentViaRegionState.HOSPITAL_SELECTION); + }); + }, + ), + ), + ], ), ); } }, ); } + + void _handleSortByLocationToggle(bool value) { + if (value) { + // User wants to sort by location — check permission & get location + final locationUtils = getIt.get(); + locationUtils.getLocation( + isShowConfirmDialog: true, + onSuccess: (latLng) { + regionalViewModel.setSortByLocation(true); + if (regionalViewModel.regionBottomSheetType == RegionBottomSheetType.FOR_REGION || regionalViewModel.regionBottomSheetType == RegionBottomSheetType.REGION_FOR_DENTAL_AND_LASER) { + myAppointmentsViewModel.getRegionMappedProjectList(); + } else if (regionalViewModel.regionBottomSheetType == RegionBottomSheetType.FOR_CLINIIC) { + myAppointmentsViewModel.getMappedDoctors(); + } + }, + onFailure: () { + showCommonBottomSheetWithoutHeight( + title: LocaleKeys.notice.tr(context: context), + context, + child: Utils.getWarningWidget( + loadingText: LocaleKeys.giveLocationPermissionForNearestList.tr(context: context), + isShowActionButtons: true, + onCancelTap: () { + Navigator.of(context).pop(); + }, + onConfirmTap: () async { + Navigator.of(context).pop(); + openAppSettings(); + }), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + regionalViewModel.setSortByLocation(false); + }, + onLocationDeniedForever: () { + showCommonBottomSheetWithoutHeight( + title: LocaleKeys.notice.tr(context: context), + context, + child: Utils.getWarningWidget( + loadingText: LocaleKeys.giveLocationPermissionForNearestList.tr(context: context), + isShowActionButtons: true, + onCancelTap: () { + Navigator.of(context).pop(); + }, + onConfirmTap: () async { + Navigator.of(context).pop(); + openAppSettings(); + }), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + regionalViewModel.setSortByLocation(false); + }, + ); + } else { + // User turned off sort by location — reset location & re-fetch + final appState = getIt.get(); + appState.resetLocation(); + regionalViewModel.setSortByLocation(false); + if (regionalViewModel.regionBottomSheetType == RegionBottomSheetType.FOR_REGION || regionalViewModel.regionBottomSheetType == RegionBottomSheetType.REGION_FOR_DENTAL_AND_LASER) { + myAppointmentsViewModel.getRegionMappedProjectList(); + } else if (regionalViewModel.regionBottomSheetType == RegionBottomSheetType.FOR_CLINIIC) { + myAppointmentsViewModel.getMappedDoctors(); + } + } + } } diff --git a/lib/presentation/authentication/saved_login_screen.dart b/lib/presentation/authentication/saved_login_screen.dart index 96db5187..dcb2c765 100644 --- a/lib/presentation/authentication/saved_login_screen.dart +++ b/lib/presentation/authentication/saved_login_screen.dart @@ -84,7 +84,7 @@ class _SavedLogin extends State { SizedBox(height: 16.h), appState.getSelectDeviceByImeiRespModelElement != null ? appState.getSelectDeviceByImeiRespModelElement!.name!.toCamelCase - .toText26(isBold: true, height: 26 / 36, color: AppColors.textColor) + .toText26(isBold: true, height: 26 / 36, color: AppColors.textColor, isEnglishOnly: true) : SizedBox(), SizedBox(height: 24.h), Container( @@ -206,7 +206,7 @@ class _SavedLogin extends State { }, backgroundColor: AppColors.primaryRedColor, borderColor: AppColors.primaryRedColor, - textColor: AppColors.textColor, + textColor: Colors.white, icon: AppAssets.sms), ), Row( diff --git a/lib/presentation/book_appointment/book_appointment_page.dart b/lib/presentation/book_appointment/book_appointment_page.dart index 4a8ada6a..1ac49f85 100644 --- a/lib/presentation/book_appointment/book_appointment_page.dart +++ b/lib/presentation/book_appointment/book_appointment_page.dart @@ -7,6 +7,7 @@ import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_export.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/location_util.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/route_extensions.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; @@ -18,6 +19,7 @@ import 'package:hmg_patient_app_new/features/immediate_livecare/immediate_liveca import 'package:hmg_patient_app_new/features/my_appointments/appointment_via_region_viewmodel.dart'; import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/presentation/appointments/my_doctors_page.dart'; import 'package:hmg_patient_app_new/presentation/appointments/widgets/faculity_selection/facility_type_selection_widget.dart'; import 'package:hmg_patient_app_new/presentation/appointments/widgets/region_bottomsheet/region_list_widget.dart' show RegionBottomSheetBody; import 'package:hmg_patient_app_new/presentation/book_appointment/doctor_profile_page.dart'; @@ -32,6 +34,7 @@ import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart' show showC import 'package:hmg_patient_app_new/widgets/custom_tab_bar.dart'; import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; +import 'package:permission_handler/permission_handler.dart'; import 'package:provider/provider.dart'; import '../appointments/widgets/hospital_bottom_sheet/hospital_bottom_sheet_body.dart'; @@ -57,16 +60,18 @@ class _BookAppointmentPageState extends State { scheduleMicrotask(() { // bookAppointmentsViewModel.selectedTabIndex = 0; bookAppointmentsViewModel.initBookAppointmentViewModel(); - bookAppointmentsViewModel.getLocation(); + // bookAppointmentsViewModel.getLocation(); immediateLiveCareViewModel.initImmediateLiveCare(); if (appState.isAuthenticated) { getIt.get().getPatientMyDoctors(); + getIt.get().getPatientFavouriteDoctors(); } }); WidgetsBinding.instance.addPostFrameCallback((_) { if (bookAppointmentsViewModel.selectedTabIndex == 1) { if (appState.isAuthenticated) { getIt.get().getPatientMyDoctors(); + getIt.get().getPatientFavouriteDoctors(); showUnKnownClinicBottomSheet(); } } else { @@ -115,18 +120,30 @@ class _BookAppointmentPageState extends State { if (appState.isAuthenticated) ...[ Consumer(builder: (context, myAppointmentsVM, child) { return myAppointmentsVM.isPatientMyDoctorsLoading - ? Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Image.network( + ? SizedBox( + height: 110.h, + child: ListView.separated( + scrollDirection: Axis.horizontal, + itemCount: 5, + shrinkWrap: true, + padding: EdgeInsets.only(left: 24.w, right: 24.w), + itemBuilder: (context, index) { + return Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Image.network( "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png", width: 64.w, height: 64.h, fit: BoxFit.cover, ).circle(100).toShimmer2(isShow: true, radius: 50.r), SizedBox(height: 8.h), - ("Dr. John Smith Smith Smith").toString().toText12(fontWeight: FontWeight.w500, isCenter: true, maxLine: 2).toShimmer2(isShow: true), - ], + ("Dr. John").toString().toText12(fontWeight: FontWeight.w500, isCenter: true, maxLine: 2).toShimmer2(isShow: true), + ], + ); + }, + separatorBuilder: (BuildContext cxt, int index) => SizedBox(width: 16.h), + ), ) : myAppointmentsVM.patientMyDoctorsList.isEmpty ? SizedBox() @@ -134,7 +151,25 @@ class _BookAppointmentPageState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ if (appState.isAuthenticated) ...[], - LocaleKeys.recentVisits.tr(context: context).toText18(isBold: true).paddingSymmetrical(24.w, 0.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + LocaleKeys.recentVisits.tr(context: context).toText18(isBold: true).paddingSymmetrical(24.w, 0.h), + Row( + children: [ + LocaleKeys.viewAll.tr(context: context).toText14(color: AppColors.primaryRedColor, weight: FontWeight.w500), + SizedBox(width: 2.h), + Icon(Icons.arrow_forward_ios, color: AppColors.primaryRedColor, size: 14.h), + ], + ).paddingSymmetrical(24.h, 0.h).onPress(() { + Navigator.of(context).push( + CustomPageRoute( + page: MyDoctorsPage(), + ), + ); + }), + ], + ), SizedBox(height: 16.h), SizedBox( height: 110.h, @@ -206,6 +241,175 @@ class _BookAppointmentPageState extends State { ], ); }), + // Favorite Doctors Section + Consumer(builder: (context, myAppointmentsVM, child) { + // Show shimmer loading state + if (myAppointmentsVM.isPatientFavouriteDoctorsLoading) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 24.h), + LocaleKeys.favouriteDoctors.tr(context: context).toText18(isBold: true).paddingSymmetrical(24.w, 0.h), + SizedBox(height: 16.h), + SizedBox( + height: 110.h, + child: ListView.separated( + scrollDirection: Axis.horizontal, + itemCount: 3, // Show 3 shimmer items + shrinkWrap: true, + padding: EdgeInsets.only(left: 24.w, right: 24.w), + itemBuilder: (context, index) { + return SizedBox( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + width: 64.h, + height: 64.h, + decoration: BoxDecoration( + color: Colors.grey[300], + shape: BoxShape.circle, + ), + ).toShimmer2(isShow: true, radius: 50.r), + SizedBox(height: 8.h), + SizedBox( + width: 80.w, + child: Container( + height: 12.h, + decoration: BoxDecoration( + color: Colors.grey[300], + borderRadius: BorderRadius.circular(4.r), + ), + ).toShimmer2(isShow: true), + ), + ], + ), + ); + }, + separatorBuilder: (BuildContext cxt, int index) => SizedBox(width: 8.h), + ), + ), + ], + ); + } + + // Show empty state or actual list + return myAppointmentsVM.patientFavouriteDoctorsList.isEmpty + ? Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 16.h), + LocaleKeys.favouriteList.tr(context: context).toText18(isBold: true).paddingSymmetrical(24.w, 0.h), + SizedBox(height: 16.h), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.h, + hasShadow: false, + ), + padding: EdgeInsets.all(16.h), + child: Column( + children: [ + Center( + child: LocaleKeys.noFavouriteDoctors.tr(context: context).toText14(color: AppColors.greyTextColor, weight: FontWeight.w500), + ), + SizedBox(height: 12.h), + CustomButton( + text: LocaleKeys.add.tr(context: context), + onPressed: () { + Navigator.of(context).push(CustomPageRoute(page: SearchDoctorByName())); + }, + backgroundColor: Color(0xffFEE9EA), + borderColor: Color(0xffFEE9EA), + textColor: Color(0xffED1C2B), + fontSize: 14.f, + fontWeight: FontWeight.w500, + padding: EdgeInsets.fromLTRB(10.h, 0, 10.h, 0), + icon: AppAssets.add_icon, + iconColor: AppColors.primaryRedColor, + height: 40.h, + ), + ], + ), + ).paddingSymmetrical(24.w, 0.h), + SizedBox(height: 24.h), + ], + ) + : Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 24.h), + LocaleKeys.favouriteDoctors.tr(context: context).toText18(isBold: true).paddingSymmetrical(24.w, 0.h), + SizedBox(height: 16.h), + SizedBox( + height: 110.h, + child: ListView.separated( + scrollDirection: Axis.horizontal, + itemCount: myAppointmentsVM.patientFavouriteDoctorsList.length, + shrinkWrap: true, + padding: EdgeInsets.only(left: 24.w, right: 24.w), + itemBuilder: (context, index) { + return AnimationConfiguration.staggeredList( + position: index, + duration: const Duration(milliseconds: 1000), + child: SlideAnimation( + horizontalOffset: 100.0, + child: FadeInAnimation( + child: SizedBox( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Image.network( + myAppointmentsVM.patientFavouriteDoctorsList[index].doctorImageUrl!, + width: 64.h, + height: 64.h, + fit: BoxFit.cover, + ).circle(100).toShimmer2(isShow: false, radius: 50.r), + SizedBox(height: 8.h), + SizedBox( + width: 80.w, + child: (myAppointmentsVM.patientFavouriteDoctorsList[index].doctorName) + .toString() + .toText12(fontWeight: FontWeight.w500, isCenter: true, maxLine: 2) + .toShimmer2(isShow: false), + ), + ], + ), + ).onPress(() async { + bookAppointmentsViewModel.setSelectedDoctor(DoctorsListResponseModel( + clinicID: myAppointmentsVM.patientFavouriteDoctorsList[index].clinicId, + projectID: myAppointmentsVM.patientFavouriteDoctorsList[index].projectId, + doctorID: myAppointmentsVM.patientFavouriteDoctorsList[index].doctorId, + )); + LoaderBottomSheet.showLoader(); + await bookAppointmentsViewModel.getDoctorProfile(onSuccess: (dynamic respData) { + LoaderBottomSheet.hideLoader(); + Navigator.of(context).push( + CustomPageRoute( + page: DoctorProfilePage(), + ), + ); + }, onError: (err) { + LoaderBottomSheet.hideLoader(); + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: err), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + }); + }), + ), + ), + ); + }, + separatorBuilder: (BuildContext cxt, int index) => SizedBox(width: 8.h), + ), + ), + ], + ); + }), ], ], ); @@ -505,24 +709,98 @@ class _BookAppointmentPageState extends State { } }, onHospitalSearch: (value) { - data.searchHospitals(value ?? ""); + data.searchHospitals(value); }, selectedFacility: data.selectedFacility, hmcCount: data.hmcCount, hmgCount: data.hmgCount, + sortByLocation: data.sortByLocation, + onSortByLocationToggle: (value) => _handleSortByLocationToggle(value, data), ); } if (data.bottomSheetState == AppointmentViaRegionState.CLINIC_SELECTION) { // Navigator.of(context).pop(); bookAppointmentsViewModel.setIsClinicsListLoading(true); bookAppointmentsViewModel.setLoadSpecificClinic(true); - bookAppointmentsViewModel.setProjectID(regionalViewModel.selectedHospital?.hospitalList.first?.mainProjectID.toString()); + bookAppointmentsViewModel.setProjectID(regionalViewModel.selectedHospital!.hospitalList.first!.mainProjectID.toString()); } else { SizedBox.shrink(); } return SizedBox.shrink(); } + void _handleSortByLocationToggle(bool value, AppointmentViaRegionViewmodel regionVM) { + if (value) { + final locationUtils = getIt.get(); + locationUtils.getLocation( + isShowConfirmDialog: true, + onSuccess: (latLng) { + regionVM.setSortByLocation(true); + _refreshHospitalListAfterApi(regionVM); + }, + onFailure: () { + showCommonBottomSheetWithoutHeight( + title: LocaleKeys.notice.tr(context: context), + context, + child: Utils.getWarningWidget( + loadingText: LocaleKeys.giveLocationPermissionForNearestList.tr(context: context), + isShowActionButtons: true, + onCancelTap: () { + Navigator.of(context).pop(); + }, + onConfirmTap: () async { + Navigator.of(context).pop(); + openAppSettings(); + }), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + regionVM.setSortByLocation(false); + }, + onLocationDeniedForever: () { + showCommonBottomSheetWithoutHeight( + title: LocaleKeys.notice.tr(context: context), + context, + child: Utils.getWarningWidget( + loadingText: LocaleKeys.giveLocationPermissionForNearestList.tr(context: context), + isShowActionButtons: true, + onCancelTap: () { + Navigator.of(context).pop(); + }, + onConfirmTap: () async { + Navigator.of(context).pop(); + openAppSettings(); + }), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + regionVM.setSortByLocation(false); + }, + ); + } else { + final appState = getIt.get(); + appState.resetLocation(); + regionVM.setSortByLocation(false); + _refreshHospitalListAfterApi(regionVM); + } + } + + void _refreshHospitalListAfterApi(AppointmentViaRegionViewmodel regionVM) { + void listener() { + if (!bookAppointmentsViewModel.isRegionListLoading) { + bookAppointmentsViewModel.removeListener(listener); + final selectedRegion = regionVM.selectedRegionId; + if (selectedRegion != null && bookAppointmentsViewModel.hospitalList?.registeredDoctorMap?[selectedRegion] != null) { + regionVM.setDisplayListAndRegionHospitalList(bookAppointmentsViewModel.hospitalList!.registeredDoctorMap![selectedRegion]); + } + } + } + bookAppointmentsViewModel.addListener(listener); + bookAppointmentsViewModel.getRegionMappedProjectList(); + } + getTitle(AppointmentViaRegionViewmodel data) { if (data.selectedRegionId == null) { return LocaleKeys.selectRegion.tr(context: context).toText20(weight: FontWeight.w600); diff --git a/lib/presentation/book_appointment/doctor_profile_page.dart b/lib/presentation/book_appointment/doctor_profile_page.dart index fdd1af0f..90d0fc99 100644 --- a/lib/presentation/book_appointment/doctor_profile_page.dart +++ b/lib/presentation/book_appointment/doctor_profile_page.dart @@ -1,5 +1,6 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:intl/intl.dart' show NumberFormat; import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; @@ -8,6 +9,7 @@ import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/widgets/appointment_calendar.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/widgets/doctor_rating_details.dart'; @@ -20,22 +22,55 @@ import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; import 'package:provider/provider.dart'; class DoctorProfilePage extends StatelessWidget { - DoctorProfilePage({super.key}); - - late AppState appState; - late BookAppointmentsViewModel bookAppointmentsViewModel; + const DoctorProfilePage({super.key}); @override Widget build(BuildContext context) { - bookAppointmentsViewModel = Provider.of(context, listen: false); - appState = getIt.get(); + final bookAppointmentsViewModel = Provider.of(context, listen: false); + final appState = getIt.get(); return Scaffold( backgroundColor: AppColors.bgScaffoldColor, body: Column( children: [ Expanded( child: CollapsingListView( - title: LocaleKeys.doctorProfile.tr(), + title: LocaleKeys.profileOnlyText.tr(), + trailing: appState.isAuthenticated + ? Consumer( + builder: (context, viewModel, child) { + return SizedBox( + width: 24.h, + height: 24.h, + child: Icon( + viewModel.isFavouriteDoctor ? Icons.bookmark : Icons.bookmark_border_outlined, + size: 36.h, + color: viewModel.isFavouriteDoctor ? AppColors.primaryRedColor : AppColors.textColor, + ).onPress(() async { + viewModel.toggleFavouriteDoctor(); + await viewModel.insertFavouriteDoctor( + patientID: appState.getAuthenticatedUser()!.patientId!, + projectID: viewModel.doctorsProfileResponseModel.projectID ?? 0, + clinicID: viewModel.doctorsProfileResponseModel.clinicID ?? 0, + doctorID: viewModel.doctorsProfileResponseModel.doctorID ?? 0, + isActive: viewModel.isFavouriteDoctor, + onSuccess: (response) { + // Successfully added/removed favorite - refresh the favorites list + getIt.get().refreshFavouriteDoctors(); + print( + viewModel.isFavouriteDoctor ? "Doctor added to favorites" : "Doctor removed from favorites", + ); + }, + onError: (error) { + // Revert the state on error + viewModel.toggleFavouriteDoctor(); + Utils.showToast(error); + }, + ); + }), + ); + }, + ) + : SizedBox.shrink(), child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -85,10 +120,12 @@ class DoctorProfilePage extends StatelessWidget { runSpacing: 4.h, children: [ AppCustomChipWidget( + backgroundColor: AppColors.whiteColor, iconColor: AppColors.ratingColorYellow, labelText: "${bookAppointmentsViewModel.doctorsProfileResponseModel.projectName}", ), AppCustomChipWidget( + backgroundColor: AppColors.whiteColor, iconColor: AppColors.ratingColorYellow, labelText: "${bookAppointmentsViewModel.doctorsProfileResponseModel.clinicDescription}", ), @@ -124,8 +161,7 @@ class DoctorProfilePage extends StatelessWidget { Utils.buildSvgWithAssets(icon: AppAssets.doctor_profile_reviews_icon, width: 48.w, height: 48.h, fit: BoxFit.contain, applyThemeColor: false), SizedBox(height: 16.h), LocaleKeys.reviews.tr(context: context).toText12(fontWeight: FontWeight.w500, color: AppColors.greyTextColor), - bookAppointmentsViewModel.doctorsProfileResponseModel.noOfPatientsRate - .toString() + NumberFormat.decimalPattern().format(bookAppointmentsViewModel.doctorsProfileResponseModel.noOfPatientsRate ?? 0) .toText16(isBold: true, color: AppColors.textColor, isUnderLine: true, decorationColor: AppColors.textColor, fontFamily: "Poppins"), ], ).onPress(() { @@ -144,9 +180,10 @@ class DoctorProfilePage extends StatelessWidget { SizedBox(height: 16.h), Divider(color: AppColors.borderOnlyColor.withValues(alpha: 0.1), height: 1.h), SizedBox(height: 16.h), - LocaleKeys.docInfo.tr(context: context).toText14(weight: FontWeight.w600, color: AppColors.textColor), + LocaleKeys.information.tr(context: context).toText14(weight: FontWeight.w600, color: AppColors.textColor), SizedBox(height: 6.h), - bookAppointmentsViewModel.doctorsProfileResponseModel.doctorProfileInfo!.toText12(fontWeight: FontWeight.w600, color: AppColors.greyTextColor), + (bookAppointmentsViewModel.doctorsProfileResponseModel.doctorProfileInfo ?? "").trim().toText12(fontWeight: FontWeight.w600, color: AppColors.greyTextColor), + SizedBox(height: 24.h), ], ).paddingSymmetrical(24.h, 0.h), ), @@ -164,7 +201,11 @@ class DoctorProfilePage extends StatelessWidget { bookAppointmentsViewModel.selectedDoctor.speciality = bookAppointmentsViewModel.doctorsProfileResponseModel.specialty; bookAppointmentsViewModel.selectedDoctor.specialityN = bookAppointmentsViewModel.doctorsProfileResponseModel.specialty; bookAppointmentsViewModel.selectedDoctor.name = bookAppointmentsViewModel.doctorsProfileResponseModel.doctorName; - bookAppointmentsViewModel.selectedDoctor.name = bookAppointmentsViewModel.doctorsProfileResponseModel.doctorName; + bookAppointmentsViewModel.selectedDoctor.doctorImageURL = bookAppointmentsViewModel.doctorsProfileResponseModel.doctorImageURL; + bookAppointmentsViewModel.selectedDoctor.nationalityFlagURL = bookAppointmentsViewModel.doctorsProfileResponseModel.nationalityFlagURL; + bookAppointmentsViewModel.selectedDoctor.clinicName = bookAppointmentsViewModel.doctorsProfileResponseModel.clinicDescription; + bookAppointmentsViewModel.selectedDoctor.projectName = bookAppointmentsViewModel.doctorsProfileResponseModel.projectName; + LoaderBottomSheet.showLoader(); bookAppointmentsViewModel.isLiveCareSchedule ? await bookAppointmentsViewModel.getLiveCareDoctorFreeSlots( diff --git a/lib/presentation/book_appointment/laser/laser_appointment.dart b/lib/presentation/book_appointment/laser/laser_appointment.dart index 3b94b0ec..c8773933 100644 --- a/lib/presentation/book_appointment/laser/laser_appointment.dart +++ b/lib/presentation/book_appointment/laser/laser_appointment.dart @@ -84,7 +84,7 @@ class LaserAppointment extends StatelessWidget { activeBackgroundColor: Color(0xffED1C2B).withValues(alpha: .1), tabs: [ CustomTabBarModel(null, LocaleKeys.malE.tr()), - CustomTabBarModel(null, "Female"), + CustomTabBarModel(null, LocaleKeys.femaleGender.tr()), ], onTabChange: (index) { var viewmodel = context.read(); @@ -93,6 +93,7 @@ class LaserAppointment extends StatelessWidget { viewmodel.getLaserClinic(); }, ), + SizedBox(height: 8.h), Selector( selector: (_, vm) => vm.selectedBodyTypeIndex, builder: (_, bodyType, __) { @@ -110,6 +111,7 @@ class LaserAppointment extends StatelessWidget { }); }, ), + SizedBox(height: 8.h), Selector>(selector: (_, vm) => vm.laserBodyPartsList, builder:(_, parts,__){ return BodyPartsListing( parts: parts, diff --git a/lib/presentation/book_appointment/laser/widgets/body_part_listing.dart b/lib/presentation/book_appointment/laser/widgets/body_part_listing.dart index c9578682..c01899cb 100644 --- a/lib/presentation/book_appointment/laser/widgets/body_part_listing.dart +++ b/lib/presentation/book_appointment/laser/widgets/body_part_listing.dart @@ -1,11 +1,17 @@ +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; import 'package:hmg_patient_app_new/features/book_appointments/models/laser_body_parts_data.dart'; import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/laser_body_parts.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; import 'package:provider/provider.dart'; +import 'package:smooth_corner/smooth_corner.dart'; class BodyPartsListing extends StatelessWidget { final List parts; @@ -46,11 +52,7 @@ class BodyPartsListing extends StatelessWidget { Visibility( visible: !isLoading, child: GridView.builder( - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 3, - childAspectRatio: 85 / 107, - crossAxisSpacing: 4.h, - mainAxisSpacing: 21.h), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3, childAspectRatio: 75 / 107, crossAxisSpacing: 4.h, mainAxisSpacing: 21.h), physics: NeverScrollableScrollPhysics(), shrinkWrap: true, itemCount: parts.length, @@ -70,7 +72,7 @@ class BodyPartsListing extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ AspectRatio( - aspectRatio: 97 / 97, + aspectRatio: 150 / 140, child: FittedBox( fit: BoxFit.fitWidth, child: Stack( @@ -93,12 +95,12 @@ class BodyPartsListing extends StatelessWidget { Container( width: 18.h, height: 18.h, - child: Icon(Icons.done, - color: Colors.white, size: 12.h), decoration: BoxDecoration( color: AppColors.primaryRedColor, borderRadius: BorderRadius.circular(30.h), ), + child: Icon(Icons.done, + color: Colors.white, size: 12.h), ), ], ), @@ -106,17 +108,46 @@ class BodyPartsListing extends StatelessWidget { ), SizedBox(height: 6.h), Expanded( - child: Text( - context - .read() - .getLaserProcedureNameWRTLanguage(parts[index]), - style: TextStyle( - fontSize: 12.f, - fontWeight: FontWeight.w600, - color: Color(0xff2B353E), - letterSpacing: -0.48, + child: Center( + child: Column( + children: [ + context.read().getLaserProcedureNameWRTLanguage(parts[index]).toText12(isBold: true, color: AppColors.textColor, isCenter: true, maxLine: 1), + SizedBox(height: 4.h), + AppCustomChipWidget( + backgroundColor: _isSelected ? AppColors.chipSecondaryLightRedColor : AppColors.whiteColor, + textColor: _isSelected ? AppColors.chipPrimaryRedBorderColor : AppColors.blackColor, + // labelText: "${parts[index].timeDuration!} ${LocaleKeys.mins.tr()}", + richText: "${parts[index].timeDuration!} ${LocaleKeys.mins.tr()}".toText12( + fontWeight: FontWeight.w500, + color: _isSelected ? AppColors.chipPrimaryRedBorderColor : AppColors.blackColor, + ), + icon: AppAssets.waiting_time_clock, + iconHasColor: true, + iconColor: _isSelected ? AppColors.chipPrimaryRedBorderColor : AppColors.blackColor, + iconSize: 16, + labelPadding: EdgeInsetsDirectional.only(start: 8.w, end: 8.w), + padding: EdgeInsets.symmetric(vertical: 8.h, horizontal: 8.w), + deleteIconSize: Size(18.w, 18.h), + shape: SmoothRectangleBorder( + borderRadius: BorderRadius.circular(10.r), + smoothness: 10, + side: BorderSide(color: _isSelected ? AppColors.chipPrimaryRedBorderColor : AppColors.borderGrayColor, width: 1), + )), + + // AppCustomChipWidget( + // labelText: "${parts[index].timeDuration!} ${LocaleKeys.mins.tr()}", + // backgroundColor: AppColors.whiteColor, + // // textColor: Utils.getCardBorderColor(myAppointmentsVM.currentQueueStatus), + // ), + // Row( + // mainAxisAlignment: MainAxisAlignment.center, + // children: [ + // "${LocaleKeys.mins.tr()}: ".toText12(isBold: true, color: AppColors.textColor, isCenter: true, maxLine: 2), + // parts[index].timeDuration!.toText12(isBold: true, color: AppColors.textColor, isCenter: true, maxLine: 2), + // ], + // ), + ], ), - maxLines: 1, ), ), ], diff --git a/lib/presentation/book_appointment/livecare/immediate_livecare_payment_details.dart b/lib/presentation/book_appointment/livecare/immediate_livecare_payment_details.dart index 7c2784de..673532ac 100644 --- a/lib/presentation/book_appointment/livecare/immediate_livecare_payment_details.dart +++ b/lib/presentation/book_appointment/livecare/immediate_livecare_payment_details.dart @@ -7,6 +7,7 @@ import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/int_extensions.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; @@ -82,9 +83,7 @@ class ImmediateLiveCarePaymentDetails extends StatelessWidget { runSpacing: 4.h, children: [ AppCustomChipWidget(labelText: "${appState.getAuthenticatedUser()!.age} ${LocaleKeys.yearsOld.tr(context: context)}"), - AppCustomChipWidget( - labelText: - "${LocaleKeys.clinic.tr()}: ${(appState.isArabic() ? immediateLiveCareVM.immediateLiveCareSelectedClinic.serviceNameN : immediateLiveCareVM.immediateLiveCareSelectedClinic.serviceName) ?? ""}"), + AppCustomChipWidget(labelText: "${LocaleKeys.gender.tr(context: context)}: ${appState.getAuthenticatedUser()?.gender == 1 ? LocaleKeys.malE.tr(context: context) : LocaleKeys.femaleGender.tr(context: context)}"), ], ), ], @@ -105,17 +104,28 @@ class ImmediateLiveCarePaymentDetails extends StatelessWidget { ), child: Padding( padding: EdgeInsets.all(16.h), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ + AppCustomChipWidget( + labelText: + "${LocaleKeys.clinic.tr()}: ${(appState.isArabic() ? immediateLiveCareVM.immediateLiveCareSelectedClinic.serviceNameN : immediateLiveCareVM.immediateLiveCareSelectedClinic.serviceName) ?? ""}"), + SizedBox(height: 16.h), + 1.divider, + SizedBox(height: 16.h), Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Utils.buildSvgWithAssets(icon: getLiveCareTypeIcon(immediateLiveCareVM.liveCareSelectedCallType), width: 32.h, height: 32.h, fit: BoxFit.contain, applyThemeColor: false), - SizedBox(width: 8.h), - getLiveCareType(context, immediateLiveCareVM.liveCareSelectedCallType).toText16(isBold: true), + Row( + children: [ + Utils.buildSvgWithAssets(icon: getLiveCareTypeIcon(immediateLiveCareVM.liveCareSelectedCallType), width: 32.h, height: 32.h, fit: BoxFit.contain, applyThemeColor: false), + SizedBox(width: 8.h), + getLiveCareType(context, immediateLiveCareVM.liveCareSelectedCallType).toText16(isBold: true), + ], + ), + Utils.buildSvgWithAssets(icon: AppAssets.edit_icon, width: 24.h, height: 24.h, fit: BoxFit.contain), ], ), - Utils.buildSvgWithAssets(icon: AppAssets.edit_icon, width: 24.h, height: 24.h, fit: BoxFit.contain), ], ), ), @@ -161,7 +171,22 @@ class ImmediateLiveCarePaymentDetails extends StatelessWidget { CustomPageRoute( page: InsuranceHomePage(), ), - ); + ) + .then((val) async { + LoaderBottomSheet.showLoader(loadingText: LocaleKeys.fetchingFeesPleaseWait.tr()); + await immediateLiveCareVM.getLiveCareImmediateAppointmentFees(onSuccess: (val) { + LoaderBottomSheet.hideLoader(); + }, onError: (err) { + LoaderBottomSheet.hideLoader(); + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: err), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + }); + }); }, backgroundColor: AppColors.primaryRedColor, borderColor: AppColors.secondaryLightRedBorderColor, @@ -202,7 +227,7 @@ class ImmediateLiveCarePaymentDetails extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - SizedBox(width: 150.h, child: Utils.getPaymentMethods()), + SizedBox(width: 200.h, child: Utils.getPaymentMethods()), Utils.getPaymentAmountWithSymbol(immediateLiveCareVM.liveCareImmediateAppointmentFeesList.total!.toText24(isBold: true), AppColors.blackColor, 17, isSaudiCurrency: (immediateLiveCareVM.liveCareImmediateAppointmentFeesList.currency!.toLowerCase() == "sar" || immediateLiveCareVM.liveCareImmediateAppointmentFeesList.currency!.toLowerCase() == "ريال")), @@ -333,15 +358,17 @@ class ImmediateLiveCarePaymentDetails extends StatelessWidget { bool cameraGranted = statuses[Permission.camera]?.isGranted ?? false; bool micGranted = statuses[Permission.microphone]?.isGranted ?? false; bool notifGranted = statuses[Permission.notification]?.isGranted ?? false; + bool alertWindowGranted = Platform.isAndroid ? (statuses[Permission.systemAlertWindow]?.isGranted ?? false) : true; // If all required permissions are already granted - if (cameraGranted && micGranted && notifGranted) return true; + if (cameraGranted && micGranted && notifGranted && alertWindowGranted) return true; // Collect only the missing permissions final missing = []; if (!cameraGranted) missing.add(Permission.camera); if (!micGranted) missing.add(Permission.microphone); if (!notifGranted) missing.add(Permission.notification); + if (Platform.isAndroid && !alertWindowGranted) missing.add(Permission.systemAlertWindow); // If any of the missing permissions are permanently denied/restricted -> open settings (single dialog) final permanent = missing.where((p) => (statuses[p]?.isPermanentlyDenied ?? false) || (statuses[p]?.isRestricted ?? false)).toList(); @@ -382,6 +409,7 @@ class ImmediateLiveCarePaymentDetails extends StatelessWidget { cameraGranted = newStatuses[Permission.camera]?.isGranted ?? cameraGranted; micGranted = newStatuses[Permission.microphone]?.isGranted ?? micGranted; notifGranted = newStatuses[Permission.notification]?.isGranted ?? notifGranted; + alertWindowGranted = newStatuses[Permission.systemAlertWindow]?.isGranted ?? alertWindowGranted; // If any requested permission is now permanently denied -> open settings final newlyPermanent = missing.where((p) => (newStatuses[p]?.isPermanentlyDenied ?? false) || (newStatuses[p]?.isRestricted ?? false)).toList(); @@ -397,7 +425,7 @@ class ImmediateLiveCarePaymentDetails extends StatelessWidget { return false; } - return cameraGranted && micGranted && notifGranted; + return cameraGranted && micGranted && notifGranted && alertWindowGranted; } // Future askVideoCallPermission() async { diff --git a/lib/presentation/book_appointment/review_appointment_page.dart b/lib/presentation/book_appointment/review_appointment_page.dart index 794b821b..a9f07b05 100644 --- a/lib/presentation/book_appointment/review_appointment_page.dart +++ b/lib/presentation/book_appointment/review_appointment_page.dart @@ -90,12 +90,12 @@ class _ReviewAppointmentPageState extends State { .toString() .toText16(isBold: true, maxlines: 1), SizedBox(width: 12.w), - Image.network( + (bookAppointmentsViewModel.selectedDoctor.nationalityFlagURL != null && bookAppointmentsViewModel.selectedDoctor.nationalityFlagURL!.isNotEmpty )? Image.network( bookAppointmentsViewModel.selectedDoctor.nationalityFlagURL ?? "https://hmgwebservices.com/Images/flag/SAU.png", width: 20.h, height: 15.h, fit: BoxFit.cover, - ), + ) : SizedBox.shrink(), ], ), SizedBox(height: 2.h), @@ -109,8 +109,8 @@ class _ReviewAppointmentPageState extends State { SizedBox(height: 12.h), Wrap( direction: Axis.horizontal, - spacing: 8.h, - runSpacing: 8.h, + spacing: 4.h, + runSpacing: 4.h, children: [ AppCustomChipWidget( labelText: "${LocaleKeys.clinic.tr(context: context)}: ${bookAppointmentsViewModel.selectedDoctor.clinicName}", @@ -156,7 +156,15 @@ class _ReviewAppointmentPageState extends State { children: [ "${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}".toText16(isBold: true), SizedBox(height: 8.h), - AppCustomChipWidget(labelText: "${appState.getAuthenticatedUser()!.age} Years Old"), + Wrap( + direction: Axis.horizontal, + spacing: 4.h, + runSpacing: 4.h, + children: [ + AppCustomChipWidget(labelText: "${appState.getAuthenticatedUser()!.age} Years Old"), + AppCustomChipWidget(labelText: "${LocaleKeys.gender.tr(context: context)}: ${appState.getAuthenticatedUser()?.gender == 1 ? LocaleKeys.malE.tr(context: context) : LocaleKeys.femaleGender.tr(context: context)}"), + ], + ), ], ), ], @@ -308,34 +316,42 @@ class _ReviewAppointmentPageState extends State { } void initiateBookAppointment() async { - LoadingUtils.showFullScreenLoader(barrierDismissible: true, isSuccessDialog: false, loadingText: LocaleKeys.bookingYourAppointment.tr(context: context)); + // LoadingUtils.showFullScreenLoader(barrierDismissible: true, isSuccessDialog: false, loadingText: bookAppointmentsViewModel.isPatientRescheduleAppointment ? LocaleKeys.reschedulingAppo.tr(context: context) : LocaleKeys.bookingYourAppointment.tr(context: context)); + LoaderBottomSheet.showLoader(loadingText: bookAppointmentsViewModel.isPatientRescheduleAppointment ? LocaleKeys.reschedulingAppo.tr(context: context) : LocaleKeys.bookingYourAppointment.tr(context: context)); myAppointmentsViewModel.setIsAppointmentDataToBeLoaded(true); if (bookAppointmentsViewModel.isLiveCareSchedule) { await bookAppointmentsViewModel.insertSpecificAppointmentForLiveCare(onError: (err) { print(err.data["ErrorEndUserMessage"]); - LoadingUtils.hideFullScreenLoader(); + LoaderBottomSheet.hideLoader(); }, onSuccess: (apiResp) async { - LoadingUtils.hideFullScreenLoader(); + LoaderBottomSheet.hideLoader(); await Future.delayed(Duration(milliseconds: 50)).then((value) async { - LoadingUtils.showFullScreenLoader(barrierDismissible: true, isSuccessDialog: true, loadingText: LocaleKeys.appointmentSuccess.tr()); - await Future.delayed(Duration(milliseconds: 4000)).then((value) { - LoadingUtils.hideFullScreenLoader(); - bookAppointmentsViewModel.setIsLiveCareSchedule(false); - Navigator.pushAndRemoveUntil( - context, - CustomPageRoute( - page: LandingNavigation(), - ), - (r) => false); - }); + // LoaderBottomSheet.showLoader(loadingText: LocaleKeys.appointmentSuccess.tr()); + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getSuccessWidget(loadingText: LocaleKeys.appointmentSuccess.tr()), + callBackFunc: () { + bookAppointmentsViewModel.setIsPatientRescheduleAppointment(false); + bookAppointmentsViewModel.setIsLiveCareSchedule(false); + Navigator.pushAndRemoveUntil( + context, + CustomPageRoute( + page: LandingNavigation(), + ), + (r) => false); + }, + isFullScreen: false, + isCloseButtonVisible: false, + isAutoDismiss: true + ); }); }); } else { //TODO: Add patient Derma package check API Here await bookAppointmentsViewModel.insertSpecificAppointment(onError: (err) { // print(err.data["ErrorEndUserMessage"]); - LoadingUtils.hideFullScreenLoader(); + LoaderBottomSheet.hideLoader(); showCommonBottomSheetWithoutHeight( title: LocaleKeys.notice.tr(context: context), context, @@ -349,19 +365,25 @@ class _ReviewAppointmentPageState extends State { isCloseButtonVisible: true, ); }, onSuccess: (apiResp) async { - LoadingUtils.hideFullScreenLoader(); + LoaderBottomSheet.hideLoader(); await Future.delayed(Duration(milliseconds: 50)).then((value) async { - LoadingUtils.showFullScreenLoader(barrierDismissible: true, isSuccessDialog: true, loadingText: LocaleKeys.appointmentSuccess.tr()); - await Future.delayed(Duration(milliseconds: 4000)).then((value) { - LoadingUtils.hideFullScreenLoader(); - bookAppointmentsViewModel.setIsLiveCareSchedule(false); - Navigator.pushAndRemoveUntil( - context, - CustomPageRoute( - page: LandingNavigation(), - ), - (r) => false); - }); + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getSuccessWidget(loadingText: LocaleKeys.appointmentSuccess.tr()).paddingSymmetrical(0.h, 24.h), + callBackFunc: () { + bookAppointmentsViewModel.setIsLiveCareSchedule(false); + bookAppointmentsViewModel.setIsPatientRescheduleAppointment(false); + Navigator.pushAndRemoveUntil( + context, + CustomPageRoute( + page: LandingNavigation(), + ), + (r) => false); + }, + isFullScreen: false, + isCloseButtonVisible: false, + isAutoDismiss: true + ); }); }); } diff --git a/lib/presentation/book_appointment/search_doctor_by_name.dart b/lib/presentation/book_appointment/search_doctor_by_name.dart index fa871f50..533229a0 100644 --- a/lib/presentation/book_appointment/search_doctor_by_name.dart +++ b/lib/presentation/book_appointment/search_doctor_by_name.dart @@ -52,7 +52,7 @@ class _SearchDoctorByNameState extends State { children: [ Expanded( child: CollapsingListView( - title: LocaleKeys.chooseDoctor.tr(), + title: LocaleKeys.chooseDoctor.tr(context: context), child: SingleChildScrollView( child: Padding( padding: EdgeInsets.symmetric(horizontal: 24.h), @@ -142,49 +142,49 @@ class _SearchDoctorByNameState extends State { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (bookAppointmentsVM.isDoctorSearchByNameStarted) - Row( - children: [ - CustomButton( - text: LocaleKeys.byClinic.tr(context: context), - onPressed: () { - bookAppointmentsVM.sortFilteredDoctorList(true); - }, - backgroundColor: bookAppointmentsVM.isSortByClinic ? AppColors.bgRedLightColor : AppColors.whiteColor, - borderColor: bookAppointmentsVM.isSortByClinic ? AppColors.primaryRedColor : AppColors.textColor.withOpacity(0.2), - textColor: bookAppointmentsVM.isSortByClinic ? AppColors.primaryRedColor : AppColors.blackColor, - fontSize: 12, - fontWeight: FontWeight.w500, - borderRadius: 10, - padding: EdgeInsets.fromLTRB(10, 0, 10, 0), - height: 40.h, - ), - SizedBox(width: 8.h), - CustomButton( - text: LocaleKeys.byHospital.tr(context: context), - onPressed: () { - bookAppointmentsVM.sortFilteredDoctorList(false); - }, - backgroundColor: bookAppointmentsVM.isSortByClinic ? AppColors.whiteColor : AppColors.bgRedLightColor, - borderColor: bookAppointmentsVM.isSortByClinic ? AppColors.textColor.withOpacity(0.2) : AppColors.primaryRedColor, - textColor: bookAppointmentsVM.isSortByClinic ? AppColors.blackColor : AppColors.primaryRedColor, - fontSize: 12, - fontWeight: FontWeight.w500, - borderRadius: 10, - padding: EdgeInsets.fromLTRB(10, 0, 10, 0), - height: 40.h, - ), - ], - ).paddingSymmetrical(0.h, 0.h), + // if (bookAppointmentsVM.isDoctorSearchByNameStarted) + // Row( + // children: [ + // CustomButton( + // text: LocaleKeys.byClinic.tr(context: context), + // onPressed: () { + // bookAppointmentsVM.sortFilteredDoctorList(true); + // }, + // backgroundColor: bookAppointmentsVM.isSortByClinic ? AppColors.bgRedLightColor : AppColors.whiteColor, + // borderColor: bookAppointmentsVM.isSortByClinic ? AppColors.primaryRedColor : AppColors.textColor.withOpacity(0.2), + // textColor: bookAppointmentsVM.isSortByClinic ? AppColors.primaryRedColor : AppColors.blackColor, + // fontSize: 12, + // fontWeight: FontWeight.w500, + // borderRadius: 10, + // padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + // height: 40.h, + // ), + // SizedBox(width: 8.h), + // CustomButton( + // text: LocaleKeys.byHospital.tr(context: context), + // onPressed: () { + // bookAppointmentsVM.sortFilteredDoctorList(false); + // }, + // backgroundColor: bookAppointmentsVM.isSortByClinic ? AppColors.whiteColor : AppColors.bgRedLightColor, + // borderColor: bookAppointmentsVM.isSortByClinic ? AppColors.textColor.withOpacity(0.2) : AppColors.primaryRedColor, + // textColor: bookAppointmentsVM.isSortByClinic ? AppColors.blackColor : AppColors.primaryRedColor, + // fontSize: 12, + // fontWeight: FontWeight.w500, + // borderRadius: 10, + // padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + // height: 40.h, + // ), + // ], + // ).paddingSymmetrical(0.h, 0.h), bookAppointmentsVM.isDoctorSearchByNameStarted ? ListView.separated( padding: EdgeInsets.only(top: 20.h), shrinkWrap: true, physics: NeverScrollableScrollPhysics(), - itemCount: bookAppointmentsVM.isDoctorsListLoading ? 5 : bookAppointmentsVM.getGroupedFilteredDoctorsList().length, + itemCount: bookAppointmentsVM.isDoctorsListLoading ? 5 : bookAppointmentsVM.filteredDoctorList.length, itemBuilder: (context, index) { - final isExpanded = bookAppointmentsVM.expandedGroupIndex == index; - final groupedDoctors = bookAppointmentsVM.getGroupedFilteredDoctorsList(); + // final isExpanded = bookAppointmentsVM.expandedGroupIndex == index; + // final groupedDoctors = bookAppointmentsVM.getGroupedFilteredDoctorsList(); return bookAppointmentsVM.isDoctorsListLoading ? DoctorCard( @@ -203,112 +203,184 @@ class _SearchDoctorByNameState extends State { duration: Duration(milliseconds: 300), curve: Curves.easeInOut, decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.h, hasShadow: true), - child: InkWell( - onTap: () { - bookAppointmentsVM.toggleGroupExpansion(index); - // After rebuild, ensure the expanded item is visible - WidgetsBinding.instance.addPostFrameCallback((_) { - final key = _itemKeys[index]; - if (key != null && key.currentContext != null && bookAppointmentsVM.expandedGroupIndex == index) { - Scrollable.ensureVisible( - key.currentContext!, - duration: Duration(milliseconds: 350), - curve: Curves.easeInOut, - alignment: 0.1, + child: Container( + key: ValueKey(index), + padding: EdgeInsets.only(top: 12.h), + child: DoctorCard( + doctorsListResponseModel: bookAppointmentsVM.filteredDoctorList[index], + isLoading: false, + bookAppointmentsViewModel: bookAppointmentsViewModel, + ).paddingSymmetrical(16.h, 0.h).onPress(() async { + bookAppointmentsVM.setSelectedDoctor(bookAppointmentsVM.filteredDoctorList[index]); + LoaderBottomSheet.showLoader(); + await bookAppointmentsVM.getDoctorProfile( + onSuccess: (dynamic respData) { + LoaderBottomSheet.hideLoader(); + Navigator.of(context).push( + CustomPageRoute( + page: DoctorProfilePage(), + ), + ); + }, + onError: (err) { + LoaderBottomSheet.hideLoader(); + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: err), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, ); - } - }); - }, - child: Padding( - padding: EdgeInsets.all(16.h), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - CustomButton( - text: "${groupedDoctors[index].length} ${'doctors'}", - onPressed: () {}, - backgroundColor: AppColors.greyColor, - borderColor: AppColors.greyColor, - textColor: AppColors.blackColor, - fontSize: 10, - fontWeight: FontWeight.w500, - borderRadius: 8, - padding: EdgeInsets.fromLTRB(10, 0, 10, 0), - height: 30.h), - Icon(isExpanded ? Icons.expand_less : Icons.expand_more), - ], - ), - SizedBox(height: 8.h), - // Clinic/Hospital name as group title - Text( - bookAppointmentsVM.isSortByClinic - ? (groupedDoctors[index].first.clinicName ?? 'Unknown') - : (groupedDoctors[index].first.projectName ?? 'Unknown'), - style: TextStyle(fontSize: 16.h, fontWeight: FontWeight.w600), - overflow: TextOverflow.ellipsis, - ), - // Expanded content - list of doctors in this group - AnimatedSwitcher( - duration: Duration(milliseconds: 400), - child: isExpanded - ? Container( - key: ValueKey(index), - padding: EdgeInsets.only(top: 12.h), - child: Column( - children: groupedDoctors[index].asMap().entries.map((entry) { - final doctorIndex = entry.key; - final doctor = entry.value; - final isLastDoctor = doctorIndex == groupedDoctors[index].length - 1; - return Column( - children: [ - DoctorCard( - doctorsListResponseModel: doctor, - isLoading: false, - bookAppointmentsViewModel: bookAppointmentsViewModel, - ).onPress(() async { - bookAppointmentsVM.setSelectedDoctor(doctor); - LoaderBottomSheet.showLoader(); - await bookAppointmentsVM.getDoctorProfile( - onSuccess: (dynamic respData) { - LoaderBottomSheet.hideLoader(); - Navigator.of(context).push( - CustomPageRoute( - page: DoctorProfilePage(), - ), - ); - }, - onError: (err) { - LoaderBottomSheet.hideLoader(); - showCommonBottomSheetWithoutHeight( - context, - child: Utils.getErrorWidget(loadingText: err), - callBackFunc: () {}, - isFullScreen: false, - isCloseButtonVisible: true, - ); - }, - ); - }), - if (!isLastDoctor) - Divider(color: AppColors.borderOnlyColor.withValues(alpha: 0.05), height: 1.h), - ], - ); - }).toList(), - ), - ) - : SizedBox.shrink(), - ), - ], - ), - ), + }, + ); + // Column( + // children: bookAppointmentsVM.doctorsList[index].map((entry) { + // final doctorIndex = entry.key; + // final doctor = entry.value; + // final isLastDoctor = doctorIndex == groupedDoctors[index].length - 1; + // return Column( + // children: [ + // DoctorCard( + // doctorsListResponseModel: doctor, + // isLoading: false, + // bookAppointmentsViewModel: bookAppointmentsViewModel, + // ).onPress(() async { + // bookAppointmentsVM.setSelectedDoctor(doctor); + // LoaderBottomSheet.showLoader(); + // await bookAppointmentsVM.getDoctorProfile( + // onSuccess: (dynamic respData) { + // LoaderBottomSheet.hideLoader(); + // Navigator.of(context).push( + // CustomPageRoute( + // page: DoctorProfilePage(), + // ), + // ); + // }, + // onError: (err) { + // LoaderBottomSheet.hideLoader(); + // showCommonBottomSheetWithoutHeight( + // context, + // child: Utils.getErrorWidget(loadingText: err), + // callBackFunc: () {}, + // isFullScreen: false, + // isCloseButtonVisible: true, + // ); + // }, + // ); + // }), + // if (!isLastDoctor) + // Divider(color: AppColors.borderOnlyColor.withValues(alpha: 0.05), height: 1.h), + // ], + // ); + // }).toList(), + // ), + }), + // InkWell( + // onTap: () { + // bookAppointmentsVM.toggleGroupExpansion(index); + // // After rebuild, ensure the expanded item is visible + // WidgetsBinding.instance.addPostFrameCallback((_) { + // final key = _itemKeys[index]; + // if (key != null && key.currentContext != null && bookAppointmentsVM.expandedGroupIndex == index) { + // Scrollable.ensureVisible( + // key.currentContext!, + // duration: Duration(milliseconds: 350), + // curve: Curves.easeInOut, + // alignment: 0.1, + // ); + // } + // }); + // }, + // child: Padding( + // padding: EdgeInsets.all(16.h), + // child: Column( + // crossAxisAlignment: CrossAxisAlignment.start, + // children: [ + // Row( + // mainAxisAlignment: MainAxisAlignment.spaceBetween, + // children: [ + // CustomButton( + // text: "${groupedDoctors[index].length} ${'doctors'}", + // onPressed: () {}, + // backgroundColor: AppColors.greyColor, + // borderColor: AppColors.greyColor, + // textColor: AppColors.blackColor, + // fontSize: 10, + // fontWeight: FontWeight.w500, + // borderRadius: 8, + // padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + // height: 30.h), + // Icon(isExpanded ? Icons.expand_less : Icons.expand_more), + // ], + // ), + // SizedBox(height: 8.h), + // // Clinic/Hospital name as group title + // Text( + // bookAppointmentsVM.isSortByClinic + // ? (groupedDoctors[index].first.clinicName ?? 'Unknown') + // : (groupedDoctors[index].first.projectName ?? 'Unknown'), + // style: TextStyle(fontSize: 16.h, fontWeight: FontWeight.w600), + // overflow: TextOverflow.ellipsis, + // ), + // // Expanded content - list of doctors in this group + // // AnimatedSwitcher( + // // duration: Duration(milliseconds: 400), + // // child: isExpanded + // // ? Container( + // // key: ValueKey(index), + // // padding: EdgeInsets.only(top: 12.h), + // // child: Column( + // // children: groupedDoctors[index].asMap().entries.map((entry) { + // // final doctorIndex = entry.key; + // // final doctor = entry.value; + // // final isLastDoctor = doctorIndex == groupedDoctors[index].length - 1; + // // return Column( + // // children: [ + // // DoctorCard( + // // doctorsListResponseModel: doctor, + // // isLoading: false, + // // bookAppointmentsViewModel: bookAppointmentsViewModel, + // // ).onPress(() async { + // // bookAppointmentsVM.setSelectedDoctor(doctor); + // // LoaderBottomSheet.showLoader(); + // // await bookAppointmentsVM.getDoctorProfile( + // // onSuccess: (dynamic respData) { + // // LoaderBottomSheet.hideLoader(); + // // Navigator.of(context).push( + // // CustomPageRoute( + // // page: DoctorProfilePage(), + // // ), + // // ); + // // }, + // // onError: (err) { + // // LoaderBottomSheet.hideLoader(); + // // showCommonBottomSheetWithoutHeight( + // // context, + // // child: Utils.getErrorWidget(loadingText: err), + // // callBackFunc: () {}, + // // isFullScreen: false, + // // isCloseButtonVisible: true, + // // ); + // // }, + // // ); + // // }), + // // if (!isLastDoctor) + // // Divider(color: AppColors.borderOnlyColor.withValues(alpha: 0.05), height: 1.h), + // // ], + // // ); + // // }).toList(), + // // ), + // // ) + // // : SizedBox.shrink(), + // // ), + // ], + // ), + // ), + // ), ), ), ), - ), - ); + )); }, separatorBuilder: (BuildContext cxt, int index) => SizedBox(height: 16.h), ) diff --git a/lib/presentation/book_appointment/select_clinic_page.dart b/lib/presentation/book_appointment/select_clinic_page.dart index aebb90fd..6812a349 100644 --- a/lib/presentation/book_appointment/select_clinic_page.dart +++ b/lib/presentation/book_appointment/select_clinic_page.dart @@ -7,6 +7,7 @@ import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/enums.dart'; +import 'package:hmg_patient_app_new/core/location_util.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; @@ -15,6 +16,8 @@ import 'package:hmg_patient_app_new/features/book_appointments/book_appointments import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/get_clinic_list_response_model.dart'; import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/get_livecare_clinics_response_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/appointment_via_region_viewmodel.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/models/facility_selection.dart'; +import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/doctor_list_api_response.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/appointments/widgets/faculity_selection/facility_type_selection_widget.dart'; import 'package:hmg_patient_app_new/presentation/appointments/widgets/hospital_bottom_sheet/hospital_bottom_sheet_body.dart'; @@ -1026,6 +1029,7 @@ class _SelectClinicPageState extends State { //17 and 235 void handleDoctorScreen(GetClinicsListResponseModel clinic) async { + regionalViewModel.flush(); if (widget.isFromRegionFlow) { //Dental Clinic Flow if (clinic.clinicID == 17) { @@ -1060,8 +1064,22 @@ class _SelectClinicPageState extends State { ); } } else { + if (clinic.clinicID == 253) { + LoaderBottomSheet.showLoader(loadingText: LocaleKeys.loadingText.tr(context: context)); + await bookAppointmentsViewModel.getLaserHospitals( + onSuccess: (_) { + LoaderBottomSheet.hideLoader(); + _showLaserHospitalBottomSheet(); + }, + onError: (err) { + LoaderBottomSheet.hideLoader(); + }, + ); + return; + } var bottomSheetType = RegionBottomSheetType.FOR_CLINIIC; - if (clinic.clinicID == 17 || clinic.clinicID == 253) { + if (clinic.clinicID == 17) { + regionalViewModel.setBottomSheetState(AppointmentViaRegionState.HOSPITAL_SELECTION); bottomSheetType = RegionBottomSheetType.REGION_FOR_DENTAL_AND_LASER; } openRegionListBottomSheet(context, bottomSheetType); @@ -1071,7 +1089,6 @@ class _SelectClinicPageState extends State { void openRegionListBottomSheet(BuildContext context, RegionBottomSheetType type) { bookAppointmentsViewModel.setProjectID(null); - regionalViewModel.flush(); regionalViewModel.setBottomSheetType(type); // AppointmentViaRegionViewmodel? viewmodel = null; showCommonBottomSheetWithoutHeight(context, title: "", titleWidget: Consumer(builder: (_, data, __) => getTitle(data)), isDismissible: false, @@ -1118,6 +1135,8 @@ class _SelectClinicPageState extends State { selectedFacility: data.selectedFacility, hmcCount: data.hmcCount, hmgCount: data.hmgCount, + sortByLocation: data.sortByLocation, + onSortByLocationToggle: (value) => _handleSortByLocationToggle(value, data), ); } if (data.bottomSheetState == AppointmentViaRegionState.DOCTOR_SELECTION) { @@ -1129,6 +1148,7 @@ class _SelectClinicPageState extends State { id = regionalViewModel.selectedHospital?.patientDoctorAppointmentList?.first.projectID?.toString() ?? ""; } if (bookAppointmentsViewModel.selectedClinic.clinicID == 17) { + bookAppointmentsViewModel.setProjectID(id); if (appState.isAuthenticated) { initDentalAppointment(); return SizedBox.shrink(); @@ -1301,4 +1321,105 @@ class _SelectClinicPageState extends State { initDentalAppointmentBookingFlow(int.parse(bookAppointmentsViewModel.currentlySelectedHospitalFromRegionFlow ?? "0")); return; } + + void _handleSortByLocationToggle(bool value, AppointmentViaRegionViewmodel regionVM) { + if (value) { + final locationUtils = getIt.get(); + locationUtils.getLocation( + isShowConfirmDialog: true, + onSuccess: (latLng) { + regionVM.setSortByLocation(true); + _refreshHospitalListAfterApi(regionVM); + }, + onFailure: () { + regionVM.setSortByLocation(false); + }, + onLocationDeniedForever: () { + regionVM.setSortByLocation(false); + }, + ); + } else { + final appState = getIt.get(); + appState.resetLocation(); + regionVM.setSortByLocation(false); + _refreshHospitalListAfterApi(regionVM); + } + } + + void _refreshHospitalListAfterApi(AppointmentViaRegionViewmodel regionVM) { + void listener() { + if (!bookAppointmentsViewModel.isRegionListLoading) { + bookAppointmentsViewModel.removeListener(listener); + final selectedRegion = regionVM.selectedRegionId; + if (selectedRegion != null && bookAppointmentsViewModel.hospitalList?.registeredDoctorMap?[selectedRegion] != null) { + regionVM.setDisplayListAndRegionHospitalList(bookAppointmentsViewModel.hospitalList!.registeredDoctorMap![selectedRegion]); + } + } + } + bookAppointmentsViewModel.addListener(listener); + + if (regionalViewModel.regionBottomSheetType == RegionBottomSheetType.FOR_REGION || regionalViewModel.regionBottomSheetType == RegionBottomSheetType.REGION_FOR_DENTAL_AND_LASER) { + bookAppointmentsViewModel.getRegionMappedProjectList(); + } else if (regionalViewModel.regionBottomSheetType == RegionBottomSheetType.FOR_CLINIIC) { + bookAppointmentsViewModel.getMappedDoctors(); + } + + // bookAppointmentsViewModel.getRegionMappedProjectList(); + } + + void _showLaserHospitalBottomSheet() { + final TextEditingController laserHospitalSearchController = TextEditingController(); + List filteredList = List.from(bookAppointmentsViewModel.laserHospitalsList); + + showCommonBottomSheetWithoutHeight( + context, + title: LocaleKeys.selectHospital.tr(context: context), + isDismissible: true, + isCloseButtonVisible: true, + child: StatefulBuilder( + builder: (context, setModalState) { + return HospitalBottomSheetBody( + searchText: laserHospitalSearchController, + displayList: filteredList, + selectedFacility: FacilitySelection.ALL, + hmcCount: bookAppointmentsViewModel.laserHospitalHmcCount, + hmgCount: bookAppointmentsViewModel.laserHospitalHmgCount, + sortByLocation: false, + onFacilityClicked: (facility) { + setModalState(() { + if (facility == FacilitySelection.ALL) { + filteredList = List.from(bookAppointmentsViewModel.laserHospitalsList); + } else if (facility == FacilitySelection.HMG) { + filteredList = bookAppointmentsViewModel.laserHospitalsList.where((h) => h.isHMC != true).toList(); + } else { + filteredList = bookAppointmentsViewModel.laserHospitalsList.where((h) => h.isHMC == true).toList(); + } + }); + }, + onHospitalClicked: (hospital) { + Navigator.of(context).pop(); // close bottom sheet + final projectID = hospital.hospitalList.isNotEmpty ? hospital.hospitalList.first.iD?.toString() : hospital.patientDoctorAppointmentList?.first.projectID?.toString(); + bookAppointmentsViewModel.setProjectID(projectID); + bookAppointmentsViewModel.resetLaserData(); + bookAppointmentsViewModel.getLaserClinic(); + Navigator.push( + context, + CustomPageRoute(page: LaserAppointment()), + ); + }, + onHospitalSearch: (value) { + setModalState(() { + if (value.isEmpty) { + filteredList = List.from(bookAppointmentsViewModel.laserHospitalsList); + } else { + filteredList = bookAppointmentsViewModel.laserHospitalsList.where((h) => h.filterName != null && h.filterName!.toLowerCase().contains(value.toLowerCase())).toList(); + } + }); + }, + ); + }, + ), + callBackFunc: () {}, + ); + } } diff --git a/lib/presentation/book_appointment/select_doctor_page.dart b/lib/presentation/book_appointment/select_doctor_page.dart index 7947fa05..4e56b083 100644 --- a/lib/presentation/book_appointment/select_doctor_page.dart +++ b/lib/presentation/book_appointment/select_doctor_page.dart @@ -220,8 +220,9 @@ class _SelectDoctorPageState extends State { ], ).paddingSymmetrical(0.h, 0.h), if (bookAppointmentsViewModel.isGetDocForHealthCal && bookAppointmentsVM.showSortFilterButtons) SizedBox(height: 16.h), - Row( - mainAxisSize: MainAxisSize.max, + bookAppointmentsVM.doctorsListGrouped.isNotEmpty + ? Row( + mainAxisSize: MainAxisSize.max, children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -241,8 +242,9 @@ class _SelectDoctorPageState extends State { }, ), ], - ), - ListView.separated( + ) + : SizedBox.shrink(), + ListView.separated( padding: EdgeInsets.only(top: 16.h), shrinkWrap: true, physics: NeverScrollableScrollPhysics(), diff --git a/lib/presentation/book_appointment/widgets/appointment_calendar.dart b/lib/presentation/book_appointment/widgets/appointment_calendar.dart index 57a045cc..56bffc38 100644 --- a/lib/presentation/book_appointment/widgets/appointment_calendar.dart +++ b/lib/presentation/book_appointment/widgets/appointment_calendar.dart @@ -99,12 +99,12 @@ class _AppointmentCalendarState extends State { showNavigationArrow: true, headerHeight: 60.h, headerStyle: CalendarHeaderStyle( - backgroundColor: AppColors.scaffoldBgColor, + backgroundColor: AppColors.transparent, textAlign: TextAlign.start, textStyle: TextStyle(fontSize: 18.f, fontWeight: FontWeight.w600, letterSpacing: -0.46, color: AppColors.primaryRedColor, fontFamily: "Poppins"), ), viewHeaderStyle: ViewHeaderStyle( - backgroundColor: AppColors.scaffoldBgColor, + // backgroundColor: AppColors.scaffoldBgColor, dayTextStyle: TextStyle(fontSize: 14.f, fontWeight: FontWeight.w600, letterSpacing: -0.46, color: AppColors.textColor, fontFamily: "Poppins"), ), view: CalendarView.month, diff --git a/lib/presentation/book_appointment/widgets/doctor_card.dart b/lib/presentation/book_appointment/widgets/doctor_card.dart index 02b2e55d..307b73ad 100644 --- a/lib/presentation/book_appointment/widgets/doctor_card.dart +++ b/lib/presentation/book_appointment/widgets/doctor_card.dart @@ -76,7 +76,7 @@ class DoctorCard extends StatelessWidget { children: [ Utils.buildSvgWithAssets(icon: AppAssets.rating_icon, width: 15.w, height: 15.h, applyThemeColor: false), SizedBox(height: 2.h), - "${isLoading ? 4.78 : doctorsListResponseModel.decimalDoctorRate}".toText11(isBold: true, color: AppColors.textColor), + "${isLoading ? 4.78 : doctorsListResponseModel.decimalDoctorRate}".toText11(isBold: true, color: AppColors.textColor, isEnglishOnly: true), ], ), ).circle(100).toShimmer2(isShow: isLoading), @@ -127,23 +127,24 @@ class DoctorCard extends StatelessWidget { spacing: 3.h, runSpacing: 4.h, children: [ - AppCustomChipWidget( - labelText: "${isLoading ? "Cardiologist" : doctorsListResponseModel.clinicName}", - ).toShimmer2(isShow: isLoading), - AppCustomChipWidget( - labelText: "${isLoading ? "Olaya Hospital" : doctorsListResponseModel.projectName}", - ).toShimmer2(isShow: isLoading), bookAppointmentsViewModel.isNearestAppointmentSelected ? doctorsListResponseModel.nearestFreeSlot != null ? AppCustomChipWidget( - // labelText: (isLoading ? "Cardiologist" : DateUtil.getDateStringForNearestSlot(doctorsListResponseModel.nearestFreeSlot)), - richText: (isLoading ? "Cardiologist" : DateUtil.getDateStringForNearestSlot(doctorsListResponseModel.nearestFreeSlot)) - .toText10(isEnglishOnly: true, color: AppColors.whiteColor), - backgroundColor: AppColors.successColor, - textColor: AppColors.whiteColor, + labelText: (isLoading ? "Cardiologist" : DateUtil.getDateStringForNearestSlot(doctorsListResponseModel.nearestFreeSlot)), + // richText: (isLoading ? "Cardiologist" : DateUtil.getDateStringForNearestSlot(doctorsListResponseModel.nearestFreeSlot)) + // .toText10(isEnglishOnly: true, color: AppColors.textColor), + backgroundColor: AppColors.successColor.withValues(alpha: .15), + isEnglishOnly: true, + textColor: AppColors.successColor, ).toShimmer2(isShow: isLoading) : SizedBox.shrink() : SizedBox.shrink(), + AppCustomChipWidget( + labelText: "${isLoading ? "Cardiologist" : doctorsListResponseModel.clinicName}", + ).toShimmer2(isShow: isLoading), + AppCustomChipWidget( + labelText: "${isLoading ? "Olaya Hospital" : doctorsListResponseModel.projectName}", + ).toShimmer2(isShow: isLoading), ], ), ], @@ -155,7 +156,7 @@ class DoctorCard extends StatelessWidget { ), ], ), - SizedBox(height: 12.h), + SizedBox(height: 16.h), CustomButton( text: LocaleKeys.bookAppo.tr(context: context), onPressed: () async { diff --git a/lib/presentation/book_appointment/widgets/doctor_rating_details.dart b/lib/presentation/book_appointment/widgets/doctor_rating_details.dart index 1a5eedfe..c146820d 100644 --- a/lib/presentation/book_appointment/widgets/doctor_rating_details.dart +++ b/lib/presentation/book_appointment/widgets/doctor_rating_details.dart @@ -1,9 +1,9 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_rating_bar/flutter_rating_bar.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; @@ -15,188 +15,302 @@ class DoctorRatingDetails extends StatelessWidget { @override Widget build(BuildContext context) { return Consumer(builder: (context, bookAppointmentsVM, child) { - return bookAppointmentsVM.isDoctorRatingDetailsLoading - ? Utils.getLoadingWidget() - : Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - bookAppointmentsVM.doctorsProfileResponseModel.actualDoctorRate!.ceilToDouble().toString().toText44(isBold: true, isEnglishOnly: true), - SizedBox(height: 4.h), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - "${bookAppointmentsVM.doctorsProfileResponseModel.noOfPatientsRate} " - .toText16(weight: FontWeight.w500, color: AppColors.greyInfoTextColor, isEnglishOnly: true), - LocaleKeys.reviews.tr(context: context) - .toText16(weight: FontWeight.w500, color: AppColors.greyInfoTextColor,), - ], - ), + if (bookAppointmentsVM.isDoctorRatingDetailsLoading) { + return Utils.getLoadingWidget(); + } - RatingBar( - initialRating: bookAppointmentsVM.doctorsProfileResponseModel.actualDoctorRate!.toDouble(), - direction: Axis.horizontal, - allowHalfRating: true, - itemCount: 5, - itemSize: 20.h, - ignoreGestures: true, - ratingWidget: RatingWidget( - full: Icon( - Icons.star, - color: AppColors.ratingColorYellow, - size: 24.h, - ), - half: Icon( - Icons.star_half, - color: AppColors.ratingColorYellow, - ), - empty: Icon( - Icons.star, - color: AppColors.ratingColorYellow, - ), - ), - tapOnlyMode: true, - unratedColor: Colors.grey[500], - itemPadding: EdgeInsets.symmetric(horizontal: 4.0), - onRatingUpdate: (rating) { - print(rating); - }, - ), - ], + final rating = bookAppointmentsVM.doctorsProfileResponseModel.decimalDoctorRate?.toDouble() ?? 0.0; + if (rating == 0 || rating == 0.0) { + return NoRatingDialog(); + } + + return DoctorRatingDialog( + averageRating: rating, + totalReviews: (bookAppointmentsVM.doctorsProfileResponseModel.noOfPatientsRate ?? 0).toInt(), + fiveStarPercent: bookAppointmentsVM.doctorDetailsList.isNotEmpty ? bookAppointmentsVM.doctorDetailsList[0].ratio.round() : 0, + fourStarPercent: bookAppointmentsVM.doctorDetailsList.length > 1 ? bookAppointmentsVM.doctorDetailsList[1].ratio.round() : 0, + threeStarPercent: bookAppointmentsVM.doctorDetailsList.length > 2 ? bookAppointmentsVM.doctorDetailsList[2].ratio.round() : 0, + twoStarPercent: bookAppointmentsVM.doctorDetailsList.length > 3 ? bookAppointmentsVM.doctorDetailsList[3].ratio.round() : 0, + oneStarPercent: bookAppointmentsVM.doctorDetailsList.length > 4 ? bookAppointmentsVM.doctorDetailsList[4].ratio.round() : 0, + ); + }); + } +} + +class DoctorRatingDialog extends StatelessWidget { + final double averageRating; + final int totalReviews; + final int fiveStarPercent; + final int fourStarPercent; + final int threeStarPercent; + final int twoStarPercent; + final int oneStarPercent; + + const DoctorRatingDialog({ + super.key, + required this.averageRating, + required this.totalReviews, + required this.fiveStarPercent, + required this.fourStarPercent, + required this.threeStarPercent, + required this.twoStarPercent, + required this.oneStarPercent, + }); + + /// Returns the color for the rating badge and progress bar based on star level. + /// 5 stars -> bright green + /// 4 stars -> slightly darker green + /// 3 stars -> dark yellow / olive + /// 2 stars -> dark orange / brown-red + /// 1 star -> red + Color _getBarColor(int stars) { + switch (stars) { + case 5: + return AppColors.ratingFiveStarColor; + case 4: + return AppColors.ratingFourStarColor; + case 3: + return AppColors.ratingThreeStarColor; + case 2: + return AppColors.ratingTwoStarColor; + case 1: + return AppColors.ratingOneStarColor; + default: + return Colors.grey; + } + } + + /// Returns the background color for the large rating badge based on the + /// overall average rating. + Color _getBadgeColor(double rating) { + if (rating == 0 || rating == 0.0) { + return AppColors.greyLightColor; // Grey color for no rating + } else if (rating >= 4.5) { + return AppColors.ratingFiveStarColor; // Bright green + } else if (rating >= 4.0) { + return AppColors.ratingBadgeFiveStarColor; // Green + } else if (rating >= 3.0) { + return AppColors.ratingThreeStarColor; + } else if (rating >= 2.0) { + return AppColors.ratingTwoStarColor; + } else { + return AppColors.ratingOneStarColor; + } + } + + @override + Widget build(BuildContext context) { + final ratingRows = [ + _RatingRowData(stars: 5, percent: fiveStarPercent), + _RatingRowData(stars: 4, percent: fourStarPercent), + _RatingRowData(stars: 3, percent: threeStarPercent), + _RatingRowData(stars: 2, percent: twoStarPercent), + _RatingRowData(stars: 1, percent: oneStarPercent), + ]; + + return Container( + width: 420, + padding: const EdgeInsets.all(0), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 130.w, + height: 140.h, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + borderRadius: 16.r, + color: _getBadgeColor(averageRating), ), - SizedBox(height: 8.h), - Container( - margin: EdgeInsets.only(top: 10.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - Container( - width: 100.0, - margin: EdgeInsets.only(top: 10.0, left: 15.0, right: 15.0), - child: Text(LocaleKeys.excellent.tr(context: context), style: TextStyle(fontSize: 13.0, color: AppColors.textColor, fontWeight: FontWeight.w600))), - getRatingLine(bookAppointmentsVM.doctorDetailsList[0].ratio, Colors.green[700]!), - ], - ), - Container( - margin: EdgeInsets.only(top: 10.0, left: 10.0, right: 10.0), - child: Text("${getRatingWidth(bookAppointmentsVM.doctorDetailsList[0].ratio).round()}%", style: TextStyle(fontSize: 14.0, color: AppColors.textColor, fontWeight: FontWeight.w600, fontFamily: "Poppins")), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + averageRating.toString(), + style: TextStyle( + fontSize: 48.f, + fontWeight: FontWeight.w700, + color: averageRating == 0 || averageRating == 0.0 ? AppColors.textColor : AppColors.whiteColor, + height: 1.1, ), - ], - ), - ), - Container( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - Container( - width: 100.0, - margin: EdgeInsets.only(top: 10.0, left: 15.0, right: 15.0), - child: Text(LocaleKeys.vGood.tr(context: context), style: TextStyle(fontSize: 13.0, color: AppColors.textColor, fontWeight: FontWeight.w600))), - getRatingLine(bookAppointmentsVM.doctorDetailsList[1].ratio, Color(0xffB7B723)), - ], + ), + const SizedBox(height: 8), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 6, ), - Container( - margin: EdgeInsets.only(top: 10.0, left: 10.0, right: 10.0), - child: Text("${bookAppointmentsVM.doctorDetailsList[1].ratio.round()}%", style: TextStyle(fontSize: 14.0, color: AppColors.textColor, fontWeight: FontWeight.w600, fontFamily: "Poppins")), + decoration: BoxDecoration( + color: averageRating == 0 || averageRating == 0.0 ? AppColors.greyF7Color : AppColors.textColor.withAlpha(20), + borderRadius: BorderRadius.circular(8), ), - ], - ), - ), - Container( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( + child: Row( children: [ - Container( - width: 100.0, - margin: EdgeInsets.only(top: 10.0, left: 15.0, right: 15.0), - child: Text(LocaleKeys.good.tr(context: context), style: TextStyle(fontSize: 13.0, color: AppColors.textColor, fontWeight: FontWeight.w600))), - getRatingLine(bookAppointmentsVM.doctorDetailsList[2].ratio, Color(0xffEBA727)), + NumberFormat.decimalPattern().format(totalReviews).toText12( + fontWeight: FontWeight.w500, + color: averageRating == 0 || averageRating == 0.0 ? AppColors.textColor : AppColors.whiteColor, + isEnglishOnly: true + ), + ' ${LocaleKeys.reviews.tr(context: context)}'.toText12( + fontWeight: FontWeight.w500, + color: averageRating == 0 || averageRating == 0.0 ? AppColors.textColor : AppColors.whiteColor, + ), ], ), - Container( - margin: EdgeInsets.only(top: 10.0, left: 10.0, right: 10.0), - child: Text("${bookAppointmentsVM.doctorDetailsList[2].ratio.round()}%", style: TextStyle(fontSize: 14.0, color: AppColors.textColor, fontWeight: FontWeight.w600, fontFamily: "Poppins")), - ), - ], - ), + // child: Text( + // '${NumberFormat.decimalPattern().format(totalReviews)} ${LocaleKeys.reviews.tr(context: context)}', + // style: TextStyle( + // fontSize: 12.f, + // fontWeight: FontWeight.w500, + // color: averageRating == 0 || averageRating == 0.0 ? AppColors.textColor : AppColors.whiteColor, + // ), + // ), + ), + ], ), - Container( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - Container( - width: 100.0, - margin: EdgeInsets.only(top: 10.0, left: 15.0, right: 15.0), - child: Text(LocaleKeys.average.tr(context: context), style: TextStyle(fontSize: 13.0, color: AppColors.textColor, fontWeight: FontWeight.w600))), - getRatingLine(bookAppointmentsVM.doctorDetailsList[3].ratio, Color(0xffEB7227)), - ], - ), - Container( - margin: EdgeInsets.only(top: 10.0, left: 10.0, right: 10.0), - child: Text("${bookAppointmentsVM.doctorDetailsList[3].ratio.round()}%", style: TextStyle(fontSize: 14.0, color: AppColors.textColor, fontWeight: FontWeight.w600, fontFamily: "Poppins")), - ), - ], - ), + ), + + const SizedBox(width: 16), + + // ── Right: Star Breakdown ── + Expanded( + child: Column( + children: ratingRows + .map( + (row) => _buildRatingRow(row.stars, row.percent), + ) + .toList(), ), - Container( - margin: EdgeInsets.only(bottom: 30.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - Container( - width: 100.0, - margin: EdgeInsets.only(top: 10.0, left: 15.0, right: 15.0), - child: Text(LocaleKeys.average.tr(context: context), style: TextStyle(fontSize: 13.0, color: AppColors.textColor, fontWeight: FontWeight.w600))), - getRatingLine(bookAppointmentsVM.doctorDetailsList[4].ratio, Color(0xffE20C0C)), - ], - ), - Container( - margin: EdgeInsets.only(top: 10.0, left: 10.0, right: 10.0), - child: Text("${bookAppointmentsVM.doctorDetailsList[4].ratio.round()}%", style: TextStyle(fontSize: 14.0, color: AppColors.textColor, fontWeight: FontWeight.w600, fontFamily: "Poppins")), - ), - ], + ), + ], + ), + const SizedBox(height: 16), + ], + ), + ); + } + + Widget _buildRatingRow(int stars, int percent) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 5), + child: Row( + children: [ + // Star icons + SizedBox( + width: 70, + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: List.generate( + stars, + (_) => Icon(Icons.star, size: 14, color: AppColors.ratingStarIconColor), + ), + ), + ), + + const SizedBox(width: 8), + Expanded( + child: ClipRRect( + borderRadius: BorderRadius.circular(6), + child: SizedBox( + height: 5.h, + child: LinearProgressIndicator( + value: percent / 100, + backgroundColor: AppColors.shimmerBaseColor, + valueColor: AlwaysStoppedAnimation( + _getBarColor(stars), ), ), - ], - ); - }); - } + ), + ), + ), + + const SizedBox(width: 8), - double getRatingWidth(num patientNumber) { - var width = patientNumber; - return width.roundToDouble(); + // Percentage text + SizedBox( + width: 36, + child: Text( + '$percent%', + textAlign: TextAlign.right, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: AppColors.ratingPercentageTextColor, + ), + ), + ), + ], + ), + ); } +} + +class _RatingRowData { + final int stars; + final int percent; + + _RatingRowData({required this.stars, required this.percent}); +} + +class NoRatingDialog extends StatelessWidget { + const NoRatingDialog({super.key}); - Widget getRatingLine(double patientNumber, Color color) { + @override + Widget build(BuildContext context) { return Container( - margin: EdgeInsets.only(top: 10.0), - child: Stack(children: [ - SizedBox( - width: 150.0, - height: 7.h, - child: Container( - color: Colors.grey[300], + width: 420, + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // Icon + Container( + width: 80, + height: 80, + decoration: BoxDecoration( + color: AppColors.lightRedButtonColor, + borderRadius: BorderRadius.circular(24), + ), + child: Icon( + Icons.star_border_rounded, + size: 48, + color: AppColors.primaryRedColor, + ), + ), + + const SizedBox(height: 20), + + // Title + Text( + LocaleKeys.noRatingAvailable.tr(context: context), + style: TextStyle( + fontSize: 22, + fontWeight: FontWeight.w700, + color: AppColors.textColor, + ), + textAlign: TextAlign.center, ), - ), - SizedBox( - width: patientNumber * 1.55, - height: 7.h, - child: Container( - color: color, + + const SizedBox(height: 12), + + // Description + Text( + LocaleKeys.doctorDoesNotHaveRating.tr(context: context), + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w400, + color: AppColors.textColorLight, + ), + textAlign: TextAlign.center, ), - ), - ]), + + const SizedBox(height: 8), + ], + ), ); } } diff --git a/lib/presentation/comprehensive_checkup/cmc_selection_review_page.dart b/lib/presentation/comprehensive_checkup/cmc_selection_review_page.dart index 840d38cf..8dcf6183 100644 --- a/lib/presentation/comprehensive_checkup/cmc_selection_review_page.dart +++ b/lib/presentation/comprehensive_checkup/cmc_selection_review_page.dart @@ -385,7 +385,7 @@ class _CmcSelectionReviewPageState extends State { isCloseButtonVisible: false, isDismissible: false, isFullScreen: false, - ); + isAutoDismiss: true); } void _handleConfirm() { diff --git a/lib/presentation/contact_us/contact_us.dart b/lib/presentation/contact_us/contact_us.dart index 70ef6e3f..fbbd11bc 100644 --- a/lib/presentation/contact_us/contact_us.dart +++ b/lib/presentation/contact_us/contact_us.dart @@ -14,6 +14,8 @@ import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/contact_us/feedback_page.dart'; import 'package:hmg_patient_app_new/presentation/contact_us/find_us_page.dart'; import 'package:hmg_patient_app_new/presentation/contact_us/live_chat_page.dart'; +import 'package:hmg_patient_app_new/presentation/home/service_info_page.dart'; +import 'package:hmg_patient_app_new/services/navigation_service.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:provider/provider.dart'; @@ -37,8 +39,8 @@ class ContactUs extends StatelessWidget { checkInOptionCard( AppAssets.call_fill, LocaleKeys.callNow.tr(), - // LocaleKeys.viewNearestHMGLocationsviewNearestHMGLocations.tr(), - "Call for immediate assistance", + LocaleKeys.callForAssistance.tr(), + // "Call for immediate assistance", ).onPress(() { launchUrl(Uri.parse("tel://" + "+966 92 006 6666")); }), @@ -80,15 +82,25 @@ class ContactUs extends StatelessWidget { LocaleKeys.liveChat.tr(), LocaleKeys.liveChatWithHMG.tr(), ).onPress(() { - locationUtils.getCurrentLocation(onSuccess: (value) { + if (getIt.get().isAuthenticated) { + Navigator.of(context).pop(); contactUsViewModel.getLiveChatProjectsList(); - Navigator.pop(context); Navigator.of(context).push( CustomPageRoute( page: LiveChatPage(), ), ); - }); + } else { + Navigator.of(getIt().navigatorKey.currentContext!).push( + CustomPageRoute( + page: ServiceInfoPage( + serviceName: LocaleKeys.liveChat.tr(), + serviceHeader: LocaleKeys.liveChatServiceHeader.tr(), + serviceDescription: LocaleKeys.liveChatServiceDescription.tr(), + serviceImage: AppAssets.livechatService), + ), + ); + } }), ], ); diff --git a/lib/presentation/contact_us/find_us_page.dart b/lib/presentation/contact_us/find_us_page.dart index 9091f74d..4b272f50 100644 --- a/lib/presentation/contact_us/find_us_page.dart +++ b/lib/presentation/contact_us/find_us_page.dart @@ -45,7 +45,7 @@ class FindUsPage extends StatelessWidget { activeBackgroundColor: AppColors.primaryRedColor.withValues(alpha: .1), tabs: [ CustomTabBarModel(null, LocaleKeys.hmgHospitals.tr()), - CustomTabBarModel(null, LocaleKeys.pharmaciesList.tr()), + CustomTabBarModel(null, LocaleKeys.hmgPharmacy.tr()), ], onTabChange: (index) { contactUsVM.setHMGHospitalsListSelected(index == 0); @@ -59,7 +59,7 @@ class FindUsPage extends StatelessWidget { children: [ LocaleKeys.sortByLocation.tr(context: context).toText14(isBold: true), SizedBox(height: 4.h), - "Sort the locations by nearest to your location".toText11(color: AppColors.textColorLight, weight: FontWeight.w500), + LocaleKeys.sortByNearestLocation.tr(context: context).toText11(color: AppColors.textColorLight, weight: FontWeight.w500), ], ), const Spacer(), diff --git a/lib/presentation/contact_us/live_chat_page.dart b/lib/presentation/contact_us/live_chat_page.dart index 0350c316..98fc9954 100644 --- a/lib/presentation/contact_us/live_chat_page.dart +++ b/lib/presentation/contact_us/live_chat_page.dart @@ -121,7 +121,9 @@ class LiveChatPage extends StatelessWidget { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - ("${appState.isArabic() ? contactUsVM.liveChatProjectsList[index].projectNameN! : contactUsVM.liveChatProjectsList[index].projectName!}\n${contactUsVM.liveChatProjectsList[index].distanceInKilometers!} KM") + ("${appState.isArabic() ? contactUsVM.liveChatProjectsList[index].projectNameN! : contactUsVM.liveChatProjectsList[index].projectName!}" + // "\n${contactUsVM.liveChatProjectsList[index].distanceInKilometers!} KM" + ) .toText14(isBold: true, color: contactUsVM.selectedLiveChatProjectIndex == index ? Colors.white : AppColors.textColor), Transform.flip( flipX: getIt.get().isArabic(), diff --git a/lib/presentation/contact_us/widgets/find_us_item_card.dart b/lib/presentation/contact_us/widgets/find_us_item_card.dart index 521775bc..1cc21939 100644 --- a/lib/presentation/contact_us/widgets/find_us_item_card.dart +++ b/lib/presentation/contact_us/widgets/find_us_item_card.dart @@ -18,6 +18,7 @@ import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; import 'package:map_launcher/map_launcher.dart'; import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher.dart'; +import 'dart:ui' as ui; class FindUsItemCard extends StatelessWidget { FindUsItemCard({super.key, required this.getHMGLocationsModel}); @@ -57,13 +58,17 @@ class FindUsItemCard extends StatelessWidget { (getHMGLocationsModel.distanceInKilometers != 0 && contactUsViewModel.hasLocationEnabled) ? Column( children: [ - AppCustomChipWidget( - labelText: "${getHMGLocationsModel.distanceInKilometers ?? ""} km", - labelPadding: EdgeInsetsDirectional.only(start: -4.h, end: 8.w), - icon: AppAssets.location_red, - // iconColor: AppColors.primaryRedColor, - // backgroundColor: AppColors.secondaryLightRedColor, - // textColor: AppColors.errorColor, + Directionality( + textDirection: ui.TextDirection.ltr, + child: AppCustomChipWidget( + labelText: "${getHMGLocationsModel.distanceInKilometers ?? ""} km", + labelPadding: EdgeInsetsDirectional.only(start: -4.h, end: 8.w), + icon: AppAssets.location_red, + isEnglishOnly: true, + // iconColor: AppColors.primaryRedColor, + // backgroundColor: AppColors.secondaryLightRedColor, + // textColor: AppColors.errorColor, + ), ), SizedBox( height: 16.h, diff --git a/lib/presentation/emergency_services/RRT/rrt_request_type_select.dart b/lib/presentation/emergency_services/RRT/rrt_request_type_select.dart index c17f3560..12996bd6 100644 --- a/lib/presentation/emergency_services/RRT/rrt_request_type_select.dart +++ b/lib/presentation/emergency_services/RRT/rrt_request_type_select.dart @@ -21,6 +21,7 @@ class RrtRequestTypeSelect extends StatelessWidget { return Column( children: [ Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ LocaleKeys.rapidResponseTeam.tr(context: context).toText20(color: AppColors.textColor, isBold: true), SizedBox( @@ -32,10 +33,9 @@ class RrtRequestTypeSelect extends StatelessWidget { borderRadius: 20.r, ), child: OptionSelection(context)), - SizedBox( - height: 24.h, - ), + SizedBox(height: 16.h), termsAndCondition(context, emergencyServicesVM), + SizedBox(height: 16.h), ], ).paddingSymmetrical(24.w, 0.h), bottomPriceContent(context, emergencyServicesVM) @@ -122,15 +122,7 @@ class RrtRequestTypeSelect extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Row( - spacing: 6.w, - children: [ - Image.asset(AppAssets.mada, width: 24.h, height: 24.h), - Image.asset(AppAssets.visa, width: 24.h, height: 24.h), - Image.asset(AppAssets.mastercard, width: 24.h, height: 24.h), - Image.asset(AppAssets.applePay, width: 24.h, height: 24.h), - ], - ), + Utils.getPaymentMethods(), Column( children: [ Divider( @@ -149,7 +141,9 @@ class RrtRequestTypeSelect extends StatelessWidget { CustomButton(text: LocaleKeys.next.tr(), onPressed: () { Navigator.pop(context); emergencyServicesVM.openRRT(); - }) + }, + isDisabled: !emergencyServicesVM.agreedToTermsAndCondition, + ) ], ).paddingAll(24.h), ), @@ -236,7 +230,6 @@ class RrtRequestTypeSelect extends StatelessWidget { value: emergencyServicesVM.agreedToTermsAndCondition, checkColor: AppColors.whiteColor, fillColor: MaterialStateProperty.resolveWith((Set states) { - print("the state is ${states}"); if (states.contains(WidgetState.selected)) { return AppColors.errorColor; } diff --git a/lib/presentation/emergency_services/call_ambulance/widgets/WayPickup.dart b/lib/presentation/emergency_services/call_ambulance/widgets/WayPickup.dart new file mode 100644 index 00000000..039241a8 --- /dev/null +++ b/lib/presentation/emergency_services/call_ambulance/widgets/WayPickup.dart @@ -0,0 +1,172 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/emergency_services/emergency_services_view_model.dart'; +import 'package:hmg_patient_app_new/features/emergency_services/models/AmbulanceCallingPlace.dart'; +import 'package:hmg_patient_app_new/features/emergency_services/models/ambulance_direction.dart'; +import 'package:hmg_patient_app_new/presentation/emergency_services/call_ambulance/widgets/transport_option_Item.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; +import 'package:provider/provider.dart'; + +import '../../../../generated/locale_keys.g.dart' show LocaleKeys; + +class WayPickup extends StatelessWidget { + final VoidCallback onTap; + const WayPickup({super.key, required this.onTap}); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LocaleKeys.selectWay.tr(context: context) + .toText24(color: AppColors.textColor, isBold: true), + SizedBox( + height: 16.h, + ), + // LocaleKeys.selectDirection.tr(context: context) + // .toText16(color: AppColors.textColor, weight: FontWeight.w600), + // SizedBox( + // height: 12.h, + // ), + Column( + children: [ + SizedBox(height: 12.h), + Column( + mainAxisAlignment: MainAxisAlignment.start, + spacing:8.h, + children: [ + TransportOptionItem( + title: LocaleKeys.oneWay.tr(context: context), + subTitle: LocaleKeys.oneWaySubtitle.tr(context: context), + firstIcon: AppAssets.to_arrow, + middleIcon: AppAssets.to_arrow, + lastIcon: AppAssets.hospital, + shouldFlipIcon: getIt.get().isArabic(), + price: context.read().selectedTransportOption?.priceTotal?.toString()??"", + onTap: () { + context + .read() + .updateDirection( AmbulanceDirection.ONE_WAY); + onTap(); + }, + ), + + TransportOptionItem( + title: LocaleKeys.twoWay.tr(context: context), + subTitle: LocaleKeys.twoWaySubtitle.tr(context: context), + firstIcon: AppAssets.dual_arrow, + middleIcon: AppAssets.dual_arrow, + lastIcon: AppAssets.hospital, + price: ((context.read().selectedTransportOption?.priceTotal??0)*2).toString(), + onTap: () { + context + .read() + .updateDirection( AmbulanceDirection.TWO_WAY); + onTap(); + }, + ), + + // Visibility( + // visible: value == AmbulanceCallingPlace.TO_HOSPITAL, + // child: Selector( + // selector: (context, viewModel) => viewModel.ambulanceDirection, + // builder: (context, directionValue, _) { + // return Column( + // crossAxisAlignment: CrossAxisAlignment.start, + // children: [ + // SizedBox(height: 16.h), + // LocaleKeys.selectWay.tr(context: context) + // .toText16(color: AppColors.textColor, weight: FontWeight.w600), + // SizedBox(height: 12.h), + // Row( + // mainAxisAlignment: MainAxisAlignment.start, + // children: [ + // Expanded( + // child: Row( + // children: [ + // Radio( + // value: AmbulanceDirection.ONE_WAY, + // groupValue: directionValue, + // onChanged: (AmbulanceDirection? newValue) { + // if (newValue != null) { + // context + // .read() + // .updateDirection(newValue); + // } + // }, + // activeColor: AppColors.primaryRedColor, + // fillColor: MaterialStateProperty.all(AppColors.primaryRedColor), + // ), + // LocaleKeys.oneWay.tr(context: context) + // .toText12(color: AppColors.textColor, fontWeight: FontWeight.w500) + // ], + // ).onPress(() { + // context + // .read() + // .updateDirection(AmbulanceDirection.ONE_WAY); + // }), + // ), + // Expanded( + // child: Row( + // children: [ + // Radio( + // value: AmbulanceDirection.TWO_WAY, + // groupValue: directionValue, + // onChanged: (AmbulanceDirection? newValue) { + // if (newValue != null) { + // context + // .read() + // .updateDirection(newValue); + // } + // }, + // activeColor: AppColors.primaryRedColor, + // fillColor: MaterialStateProperty.all(AppColors.primaryRedColor), + // ), + // LocaleKeys.twoWay.tr(context: context) + // .toText14(color: AppColors.textColor, weight: FontWeight.w500) + // ], + // ).onPress(() { + // context + // + // .read() + // .updateDirection(AmbulanceDirection.TWO_WAY); + // }), + // ), + // ], + // ), + // ], + // ); + // }, + // ), + ], + ), + ], + ), + // Selector( + // selector: (context, viewModel) => viewModel.callingPlace, + // builder: (context, value, _) { + // return ; + // }, + // ), + SizedBox( + height: 16.h, + ), + // CustomButton( + // text: LocaleKeys.confirm.tr(context: context), + // onPressed: onTap, + // backgroundColor: AppColors.primaryRedColor, + // borderColor: AppColors.primaryRedColor, + // textColor: AppColors.whiteColor, + // iconColor: AppColors.whiteColor, + // ), + ], + ); + } +} \ No newline at end of file diff --git a/lib/presentation/emergency_services/call_ambulance/widgets/ambulance_option_selection_bottomsheet.dart b/lib/presentation/emergency_services/call_ambulance/widgets/ambulance_option_selection_bottomsheet.dart index be1d3800..a99a7bbd 100644 --- a/lib/presentation/emergency_services/call_ambulance/widgets/ambulance_option_selection_bottomsheet.dart +++ b/lib/presentation/emergency_services/call_ambulance/widgets/ambulance_option_selection_bottomsheet.dart @@ -29,7 +29,7 @@ class AmbulanceOptionSelectionBottomSheet extends StatelessWidget { itemBuilder: (_, index) => TransportOptionItem( title: Utils.getTextWRTCurrentLanguage(data[index].text, data[index].textN), subTitle: "", - firstIcon: AppAssets.location_pickup, + firstIcon: AppAssets.ambulance, middleIcon: AppAssets.to_arrow, lastIcon: AppAssets.hospital, price: data[index].priceTotal.toString(), diff --git a/lib/presentation/emergency_services/call_ambulance/widgets/pickup_location.dart b/lib/presentation/emergency_services/call_ambulance/widgets/pickup_location.dart index 56a94e0f..d76e6192 100644 --- a/lib/presentation/emergency_services/call_ambulance/widgets/pickup_location.dart +++ b/lib/presentation/emergency_services/call_ambulance/widgets/pickup_location.dart @@ -1,11 +1,13 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_export.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/emergency_services/emergency_services_view_model.dart'; import 'package:hmg_patient_app_new/features/emergency_services/models/AmbulanceCallingPlace.dart'; import 'package:hmg_patient_app_new/features/emergency_services/models/ambulance_direction.dart'; +import 'package:hmg_patient_app_new/presentation/emergency_services/call_ambulance/widgets/transport_option_Item.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:provider/provider.dart'; @@ -13,7 +15,7 @@ import 'package:provider/provider.dart'; import '../../../../generated/locale_keys.g.dart' show LocaleKeys; class PickupLocation extends StatelessWidget { - final VoidCallback onTap; + final Function(AmbulanceCallingPlace) onTap; const PickupLocation({super.key, required this.onTap}); @override @@ -26,159 +28,189 @@ class PickupLocation extends StatelessWidget { SizedBox( height: 16.h, ), - LocaleKeys.selectDirection.tr(context: context) - .toText16(color: AppColors.textColor, weight: FontWeight.w600), - SizedBox( - height: 12.h, - ), - Selector( - selector: (context, viewModel) => viewModel.callingPlace, - builder: (context, value, _) { - return Column( + // LocaleKeys.selectDirection.tr(context: context) + // .toText16(color: AppColors.textColor, weight: FontWeight.w600), + // SizedBox( + // height: 12.h, + // ), + Column( + children: [ + SizedBox(height: 12.h), + Column( + mainAxisAlignment: MainAxisAlignment.start, + spacing:8.h, children: [ - SizedBox(height: 12.h), - Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Expanded( - child: Row( - children: [ - Radio( - value: AmbulanceCallingPlace.TO_HOSPITAL, - groupValue: value, - onChanged: (AmbulanceCallingPlace? newValue) { - if (newValue != null) { - context - .read() - .updateCallingPlace(newValue); - } - }, - activeColor: AppColors.primaryRedColor, - fillColor: MaterialStateProperty.all(AppColors.primaryRedColor), - ), - LocaleKeys.toHospital.tr(context: context) - .toText14(color: AppColors.textColor, weight: FontWeight.w500) - ], - ).onPress(() { - context - .read() - .updateCallingPlace(AmbulanceCallingPlace.TO_HOSPITAL); - }), - ), - Expanded( - child: Row( - children: [ - Radio( - value: AmbulanceCallingPlace.FROM_HOSPITAL, - groupValue: value, - onChanged: (AmbulanceCallingPlace? newValue) { - if (newValue != null) { - context - .read() - .updateCallingPlace(newValue); - } - }, - activeColor: AppColors.primaryRedColor, - fillColor: MaterialStateProperty.all(AppColors.primaryRedColor), - ), - LocaleKeys.fromHospital.tr(context: context) - .toText14(color: AppColors.textColor, weight: FontWeight.w500) - ], - ).onPress(() { - context - .read() - .updateCallingPlace(AmbulanceCallingPlace.FROM_HOSPITAL); - }), - ), - ], + TransportOptionItem( + title: LocaleKeys.toHospital.tr(context: context), + subTitle: LocaleKeys.toHospitalSubtitle.tr(context: context), + firstIcon: AppAssets.homeBottomFill, + middleIcon: AppAssets.to_arrow, + lastIcon: AppAssets.hospital, + price: context.read().selectedTransportOption?.priceTotal?.toString()??"", + onTap: () { + context.read() + .updateCallingPlace(AmbulanceCallingPlace.TO_HOSPITAL); + onTap(AmbulanceCallingPlace.TO_HOSPITAL); + }, ), - Visibility( - visible: value == AmbulanceCallingPlace.TO_HOSPITAL, - child: Selector( - selector: (context, viewModel) => viewModel.ambulanceDirection, - builder: (context, directionValue, _) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox(height: 16.h), - LocaleKeys.selectWay.tr(context: context) - .toText16(color: AppColors.textColor, weight: FontWeight.w600), - SizedBox(height: 12.h), - Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Expanded( - child: Row( - children: [ - Radio( - value: AmbulanceDirection.ONE_WAY, - groupValue: directionValue, - onChanged: (AmbulanceDirection? newValue) { - if (newValue != null) { - context - .read() - .updateDirection(newValue); - } - }, - activeColor: AppColors.primaryRedColor, - fillColor: MaterialStateProperty.all(AppColors.primaryRedColor), - ), - LocaleKeys.oneWay.tr(context: context) - .toText12(color: AppColors.textColor, fontWeight: FontWeight.w500) - ], - ).onPress(() { - context - .read() - .updateDirection(AmbulanceDirection.ONE_WAY); - }), - ), - Expanded( - child: Row( - children: [ - Radio( - value: AmbulanceDirection.TWO_WAY, - groupValue: directionValue, - onChanged: (AmbulanceDirection? newValue) { - if (newValue != null) { - context - .read() - .updateDirection(newValue); - } - }, - activeColor: AppColors.primaryRedColor, - fillColor: MaterialStateProperty.all(AppColors.primaryRedColor), - ), - LocaleKeys.twoWay.tr(context: context) - .toText14(color: AppColors.textColor, weight: FontWeight.w500) - ], - ).onPress(() { - context - .read() - .updateDirection(AmbulanceDirection.TWO_WAY); - }), - ), - ], - ), - ], - ); - }, - ), - ) + + TransportOptionItem( + title: LocaleKeys.fromHospital.tr(context: context), + subTitle: LocaleKeys.fromHospitalSubtitle.tr(context: context), + firstIcon: AppAssets.hospital, + middleIcon: AppAssets.to_arrow, + lastIcon: AppAssets.hospital, + price: context.read().selectedTransportOption?.priceTotal?.toString()??"", + onTap: () { + context.read().updateCallingPlace(AmbulanceCallingPlace.FROM_HOSPITAL); + onTap(AmbulanceCallingPlace.FROM_HOSPITAL); + }, + ), + // Expanded( + // child: Row( + // children: [ + // Radio( + // value: AmbulanceCallingPlace.TO_HOSPITAL, + // groupValue: value, + // onChanged: (AmbulanceCallingPlace? newValue) { + // if (newValue != null) { + // context + // .read() + // .updateCallingPlace(newValue); + // } + // }, + // activeColor: AppColors.primaryRedColor, + // fillColor: MaterialStateProperty.all(AppColors.primaryRedColor), + // ), + // LocaleKeys.toHospital.tr(context: context) + // .toText14(color: AppColors.textColor, weight: FontWeight.w500) + // ], + // ).onPress(() { + // context + // .read() + // .updateCallingPlace(AmbulanceCallingPlace.TO_HOSPITAL); + // }), + // ), + // Expanded( + // child: Row( + // children: [ + // Radio( + // value: AmbulanceCallingPlace.FROM_HOSPITAL, + // groupValue: value, + // onChanged: (AmbulanceCallingPlace? newValue) { + // if (newValue != null) { + // context + // .read() + // .updateCallingPlace(newValue); + // } + // }, + // activeColor: AppColors.primaryRedColor, + // fillColor: MaterialStateProperty.all(AppColors.primaryRedColor), + // ), + // LocaleKeys.fromHospital.tr(context: context) + // .toText14(color: AppColors.textColor, weight: FontWeight.w500) + // ], + // ).onPress(() { + // context + // .read() + // .updateCallingPlace(AmbulanceCallingPlace.FROM_HOSPITAL); + // }), + // ), + // ], + // ), + // Visibility( + // visible: value == AmbulanceCallingPlace.TO_HOSPITAL, + // child: Selector( + // selector: (context, viewModel) => viewModel.ambulanceDirection, + // builder: (context, directionValue, _) { + // return Column( + // crossAxisAlignment: CrossAxisAlignment.start, + // children: [ + // SizedBox(height: 16.h), + // LocaleKeys.selectWay.tr(context: context) + // .toText16(color: AppColors.textColor, weight: FontWeight.w600), + // SizedBox(height: 12.h), + // Row( + // mainAxisAlignment: MainAxisAlignment.start, + // children: [ + // Expanded( + // child: Row( + // children: [ + // Radio( + // value: AmbulanceDirection.ONE_WAY, + // groupValue: directionValue, + // onChanged: (AmbulanceDirection? newValue) { + // if (newValue != null) { + // context + // .read() + // .updateDirection(newValue); + // } + // }, + // activeColor: AppColors.primaryRedColor, + // fillColor: MaterialStateProperty.all(AppColors.primaryRedColor), + // ), + // LocaleKeys.oneWay.tr(context: context) + // .toText12(color: AppColors.textColor, fontWeight: FontWeight.w500) + // ], + // ).onPress(() { + // context + // .read() + // .updateDirection(AmbulanceDirection.ONE_WAY); + // }), + // ), + // Expanded( + // child: Row( + // children: [ + // Radio( + // value: AmbulanceDirection.TWO_WAY, + // groupValue: directionValue, + // onChanged: (AmbulanceDirection? newValue) { + // if (newValue != null) { + // context + // .read() + // .updateDirection(newValue); + // } + // }, + // activeColor: AppColors.primaryRedColor, + // fillColor: MaterialStateProperty.all(AppColors.primaryRedColor), + // ), + // LocaleKeys.twoWay.tr(context: context) + // .toText14(color: AppColors.textColor, weight: FontWeight.w500) + // ], + // ).onPress(() { + // context + // + // .read() + // .updateDirection(AmbulanceDirection.TWO_WAY); + // }), + // ), + // ], + // ), + // ], + // ); + // }, + // ), ], - ); - }, + ), + ], ), + // Selector( + // selector: (context, viewModel) => viewModel.callingPlace, + // builder: (context, value, _) { + // return ; + // }, + // ), SizedBox( height: 16.h, ), - CustomButton( - text: LocaleKeys.confirm.tr(context: context), - onPressed: onTap, - backgroundColor: AppColors.primaryRedColor, - borderColor: AppColors.primaryRedColor, - textColor: AppColors.whiteColor, - iconColor: AppColors.whiteColor, - ), + // CustomButton( + // text: LocaleKeys.confirm.tr(context: context), + // onPressed: onTap, + // backgroundColor: AppColors.primaryRedColor, + // borderColor: AppColors.primaryRedColor, + // textColor: AppColors.whiteColor, + // iconColor: AppColors.whiteColor, + // ), ], ); } diff --git a/lib/presentation/emergency_services/call_ambulance/widgets/transport_option_Item.dart b/lib/presentation/emergency_services/call_ambulance/widgets/transport_option_Item.dart index 9a959ebb..13c4c919 100644 --- a/lib/presentation/emergency_services/call_ambulance/widgets/transport_option_Item.dart +++ b/lib/presentation/emergency_services/call_ambulance/widgets/transport_option_Item.dart @@ -1,11 +1,14 @@ import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; +import 'package:provider/provider.dart'; class TransportOptionItem extends StatelessWidget { final String title; @@ -15,6 +18,7 @@ class TransportOptionItem extends StatelessWidget { final String lastIcon; final String price; final VoidCallback onTap; + final bool shouldFlipIcon; const TransportOptionItem( {super.key, @@ -24,6 +28,7 @@ class TransportOptionItem extends StatelessWidget { required this.middleIcon, required this.lastIcon, required this.price, + this.shouldFlipIcon = false, required this.onTap}); @override @@ -39,14 +44,14 @@ class TransportOptionItem extends StatelessWidget { spacing: 16.h, children: [ headerSection(), - titleSection(), + titleSection(context), ], )).onPress((){ onTap(); }); } - titleSection() { + titleSection(BuildContext context) { return Row( children: [ Expanded( @@ -55,13 +60,11 @@ class TransportOptionItem extends StatelessWidget { children: [ title.toText16( color: AppColors.textColor, weight: FontWeight.w600), - // subTitle.toText12( - // color: AppColors.greyTextColor, fontWeight: FontWeight.w500), + subTitle.toText12(color: AppColors.greyTextColor, fontWeight: FontWeight.w500), ], ), ), - Utils.buildSvgWithAssets( - icon: AppAssets.forward_arrow_medium, width: 24.h, height: 24.h) + Transform.flip(flipX: getIt.get().isArabic(), child: Utils.buildSvgWithAssets(icon: AppAssets.forward_arrow_medium, width: 24.h, height: 24.h)) ], ); } @@ -70,20 +73,19 @@ class TransportOptionItem extends StatelessWidget { return Row( mainAxisAlignment: MainAxisAlignment.end, children: [ - // Expanded( - // child: Row( - // children: [ - // buildIcon(firstIcon), - // Utils.buildSvgWithAssets( - // icon: middleIcon, width: 24.h, height: 24.h).paddingAll(8.h), - // buildIcon(lastIcon) - // ], - // ), - // ), - Utils.getPaymentAmountWithSymbol2( - num.tryParse(price) ?? 0.0, - fontSize: 18.f, - letterSpacing:-2 + Expanded( + child: Row( + children: [ + Transform.flip(flipX: getIt.get().isArabic() && shouldFlipIcon, child: buildIcon(firstIcon)), + // Utils.buildSvgWithAssets( + // icon: middleIcon, width: 24.h, height: 24.h).paddingAll(8.h), + // buildIcon(lastIcon) + ], + ), + ), + Visibility( + visible: price.isNotEmpty, + child: Utils.getPaymentAmountWithSymbol2(num.tryParse(price) ?? 0.0, fontSize: 18.f, letterSpacing: -2), ), ], ); @@ -98,7 +100,7 @@ class TransportOptionItem extends StatelessWidget { backgroundColor: AppColors.greyColor, iconColor: AppColors.greyTextColor, borderColor: Colors.transparent, - height: 40.h, + height: 42.h, ); } } diff --git a/lib/presentation/emergency_services/emergency_services_page.dart b/lib/presentation/emergency_services/emergency_services_page.dart index 7970d819..c8b01d16 100644 --- a/lib/presentation/emergency_services/emergency_services_page.dart +++ b/lib/presentation/emergency_services/emergency_services_page.dart @@ -1,6 +1,8 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; @@ -10,9 +12,12 @@ import 'package:hmg_patient_app_new/features/emergency_services/models/OrderDisp import 'package:hmg_patient_app_new/features/location/location_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/emergency_services/RRT/rrt_request_type_select.dart'; +import 'package:hmg_patient_app_new/presentation/emergency_services/call_ambulance/widgets/WayPickup.dart'; import 'package:hmg_patient_app_new/presentation/emergency_services/call_ambulance/widgets/ambulance_option_selection_bottomsheet.dart'; import 'package:hmg_patient_app_new/presentation/emergency_services/call_ambulance/widgets/pickup_location.dart'; +import 'package:hmg_patient_app_new/presentation/emergency_services/er_online_checkin/er_online_checkin_home.dart'; import 'package:hmg_patient_app_new/presentation/emergency_services/history/er_history_listing.dart'; +import 'package:hmg_patient_app_new/presentation/home/service_info_page.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; @@ -22,23 +27,48 @@ import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:lottie/lottie.dart'; import 'package:provider/provider.dart'; -class EmergencyServicesPage extends StatelessWidget { +import '../../features/emergency_services/models/AmbulanceCallingPlace.dart' show AmbulanceCallingPlace; + +class EmergencyServicesPage extends StatefulWidget { EmergencyServicesPage({super.key}); + @override + State createState() => _EmergencyServicesPageState(); +} + +class _EmergencyServicesPageState extends State { late EmergencyServicesViewModel emergencyServicesViewModel; - _handleConfirmationBottomSheet() {} + late AppState appState; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (appState.isAuthenticated) { + emergencyServicesViewModel.flushData(); + emergencyServicesViewModel.getTransportationOrders( + showLoader: false, + ); + emergencyServicesViewModel.getRRTOrders( + showLoader: false, + ); + } + }); + } @override Widget build(BuildContext context) { emergencyServicesViewModel = Provider.of(context, listen: false); - + appState = getIt.get(); return CollapsingListView( title: LocaleKeys.emergencyServices.tr(), - history: () { - emergencyServicesViewModel.changeOrderDisplayItems(OrderDislpay.ALL); - Navigator.of(context).push(CustomPageRoute(page: ErHistoryListing(), direction: AxisDirection.up)); - }, + history: appState.isAuthenticated + ? () { + emergencyServicesViewModel.changeOrderDisplayItems(OrderDislpay.ALL); + Navigator.of(context).push(CustomPageRoute(page: ErHistoryListing(), direction: AxisDirection.up)); + } + : null, child: Padding( padding: EdgeInsets.all(24.h), child: Column( @@ -60,74 +90,86 @@ class EmergencyServicesPage extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ LocaleKeys.callAmbulance.tr().toText16(isBold: true, color: AppColors.blackColor), - LocaleKeys.requestAmbulanceInEmergency.tr() - .toText12(color: AppColors.greyTextColor, fontWeight: FontWeight.w500), + LocaleKeys.requestAmbulanceInEmergency.tr().toText12(color: AppColors.greyTextColor, fontWeight: FontWeight.w500), ], ), ), SizedBox(width: 12.h), - Utils.buildSvgWithAssets(icon: AppAssets.forward_chevron_icon, width: 13.h, height: 13.h), + Transform.flip( + flipX: appState.isArabic(), + child: Utils.buildSvgWithAssets(icon: AppAssets.forward_chevron_icon, width: 13.h, height: 13.h), + ), ], ).onPress(() { - showCommonBottomSheetWithoutHeight( - context, - child: Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.primaryRedColor, - borderRadius: 24.h, - ), - child: Padding( - padding: EdgeInsets.all(24.h), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - "".toText14(), - Utils.buildSvgWithAssets( - icon: AppAssets.cancel_circle_icon, - iconColor: Colors.white, - width: 24.h, - height: 24.h, - fit: BoxFit.contain, - ).onPress(() { + if(appState.isAuthenticated) { + showCommonBottomSheetWithoutHeight( + context, + child: Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.primaryRedColor, + borderRadius: 24.h, + ), + child: Padding( + padding: EdgeInsets.all(24.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + "".toText14(), + Utils.buildSvgWithAssets( + icon: AppAssets.cancel_circle_icon, + iconColor: Colors.white, + width: 24.h, + height: 24.h, + fit: BoxFit.contain, + ).onPress(() { + Navigator.of(context).pop(); + }), + ], + ), + Lottie.asset(AppAnimations.ambulanceAlert, repeat: false, reverse: false, frameRate: FrameRate(60), width: 120.h, height: 120.h, fit: BoxFit.contain), + SizedBox(height: 8.h), + LocaleKeys.confirmation.tr().toText28(color: Colors.white, isBold: true), + SizedBox(height: 8.h), + LocaleKeys.areYouSureYouWantToCallAmbulance.tr().toText14(color: Colors.white, weight: FontWeight.w500), + SizedBox(height: 24.h), + CustomButton( + text: LocaleKeys.confirm.tr(context: context), + onPressed: () async { Navigator.of(context).pop(); - }), - ], - ), - Lottie.asset(AppAnimations.ambulanceAlert, - repeat: false, reverse: false, frameRate: FrameRate(60), width: 120.h, height: 120.h, fit: BoxFit.contain), - SizedBox(height: 8.h), - LocaleKeys.confirmation.tr().toText28(color: Colors.white, isBold: true), - SizedBox(height: 8.h), - LocaleKeys.areYouSureYouWantToCallAmbulance.tr() - .toText14(color: Colors.white, weight: FontWeight.w500), - SizedBox(height: 24.h), - CustomButton( - text: LocaleKeys.confirm.tr(context: context), - onPressed: () async { - Navigator.of(context).pop(); - await emergencyServicesViewModel.getTransportationOption(); - openTranportationSelectionBottomSheet(context); - }, - backgroundColor: Colors.white, - borderColor: Colors.white, - textColor: AppColors.primaryRedColor, - icon: AppAssets.checkmark_icon, - iconColor: AppColors.primaryRedColor, - ), - SizedBox(height: 8.h), - ], + await emergencyServicesViewModel.getTransportationOption(); + openTranportationSelectionBottomSheet(context); + }, + backgroundColor: Colors.white, + borderColor: Colors.white, + textColor: AppColors.primaryRedColor, + icon: AppAssets.checkmark_icon, + iconColor: AppColors.primaryRedColor, + ), + SizedBox(height: 8.h), + ], + ), ), ), - ), - isFullScreen: false, - isCloseButtonVisible: false, - hasBottomPadding: false, - backgroundColor: AppColors.primaryRedColor, - callBackFunc: () {}, - ); + isFullScreen: false, + isCloseButtonVisible: false, + hasBottomPadding: false, + backgroundColor: AppColors.primaryRedColor, + callBackFunc: () {}, + ); + } else { + Navigator.of(context).push( + CustomPageRoute( + page: ServiceInfoPage( + serviceName: LocaleKeys.emergencyServices.tr(), + serviceHeader: LocaleKeys.emergencyServiceHeader.tr(), + serviceDescription: LocaleKeys.emergencyServiceDescription.tr(), + serviceImage: AppAssets.emergencyService), + ), + ); + } }), ), SizedBox(height: 16.h), @@ -147,13 +189,15 @@ class EmergencyServicesPage extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ LocaleKeys.nearester.tr(context: context).toText16(isBold: true, color: AppColors.blackColor), - LocaleKeys.getDetailsOfNearestBranch.tr() - .toText12(color: AppColors.greyTextColor, fontWeight: FontWeight.w500), + LocaleKeys.getDetailsOfNearestBranch.tr().toText12(color: AppColors.greyTextColor, fontWeight: FontWeight.w500), ], ), ), SizedBox(width: 12.h), - Utils.buildSvgWithAssets(icon: AppAssets.forward_chevron_icon, width: 13.h, height: 13.h), + Transform.flip( + flipX: appState.isArabic(), + child: Utils.buildSvgWithAssets(icon: AppAssets.forward_chevron_icon, width: 13.h, height: 13.h), + ), ], ).onPress(() { context.read().navigateToNearestERPage(); @@ -176,86 +220,109 @@ class EmergencyServicesPage extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ LocaleKeys.rapidResponseTeam.tr(context: context).toText16(isBold: true, color: AppColors.blackColor), - "Comprehensive medical service for all sorts of urgent and stable cases" - .toText12(color: AppColors.greyTextColor, fontWeight: FontWeight.w500), + "Comprehensive medical service for all sorts of urgent and stable cases".toText12(color: AppColors.greyTextColor, fontWeight: FontWeight.w500), ], ), ), SizedBox(width: 12.h), - Utils.buildSvgWithAssets(icon: AppAssets.forward_chevron_icon, width: 13.h, height: 13.h), + Transform.flip(flipX: appState.isArabic(), child: Utils.buildSvgWithAssets(icon: AppAssets.forward_chevron_icon, width: 13.h, height: 13.h)), ], ).onPress(() { - showCommonBottomSheetWithoutHeight( - context, - child: Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.primaryRedColor, - borderRadius: 24.h, - ), - child: Padding( - padding: EdgeInsets.all(24.h), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Lottie.asset(AppAnimations.ambulanceAlert, repeat: false, reverse: false, frameRate: FrameRate(60), width: 120.h, height: 120.h, fit: BoxFit.contain), - SizedBox(height: 8.h), - LocaleKeys.confirm.tr().toText28(color: Colors.white, isBold: true), - SizedBox(height: 8.h), - LocaleKeys.areYouSureYouWantToCallRRT.tr() - .toText14(color: Colors.white, weight: FontWeight.w500), - SizedBox(height: 24.h), - CustomButton( - text: LocaleKeys.confirm.tr(context: context), - onPressed: () async { - Navigator.of(context).pop(); - - LoaderBottomSheet.showLoader(); - emergencyServicesViewModel.clearRRTData(); - await emergencyServicesViewModel.getRRTProcedures(onSuccess: (val) { - LoaderBottomSheet.hideLoader(); - showCommonBottomSheetWithoutHeight( - padding: EdgeInsets.only(top: 24.h), - titleWidget: Transform.flip( - flipX: emergencyServicesViewModel.isArabic, - child: Utils.buildSvgWithAssets( - icon: AppAssets.arrow_back, - iconColor: Color(0xff2B353E), - fit: BoxFit.contain, - ), - ).onPress(() { - - Navigator.pop(context); - }), - // title: "Rapid Response Team (RRT)".needTranslation, - context, - child: RrtRequestTypeSelect(), - isFullScreen: false, - isCloseButtonVisible: true, - hasBottomPadding: false, - backgroundColor: AppColors.bottomSheetBgColor, - callBackFunc: () {}, - ); - }); - }, - backgroundColor: Colors.white, - borderColor: Colors.white, - textColor: AppColors.primaryRedColor, - icon: AppAssets.checkmark_icon, - iconColor: AppColors.primaryRedColor, - ), - SizedBox(height: 8.h), - ], + if(appState.isAuthenticated) { + showCommonBottomSheetWithoutHeight( + context, + child: Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.primaryRedColor, + borderRadius: 24.h, + ), + child: Padding( + padding: EdgeInsets.all(24.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + "".toText14(), + Utils.buildSvgWithAssets( + icon: AppAssets.cancel_circle_icon, + iconColor: Colors.white, + width: 24.h, + height: 24.h, + fit: BoxFit.contain, + ).onPress(() { + Navigator.of(context).pop(); + }), + ], + ), + Lottie.asset(AppAnimations.ambulanceAlert, repeat: false, reverse: false, frameRate: FrameRate(60), width: 120.h, height: 120.h, fit: BoxFit.contain), + SizedBox(height: 8.h), + LocaleKeys.confirm.tr().toText28(color: Colors.white, isBold: true), + SizedBox(height: 8.h), + LocaleKeys.areYouSureYouWantToCallRRT.tr().toText14(color: Colors.white, weight: FontWeight.w500), + SizedBox(height: 24.h), + CustomButton( + text: LocaleKeys.confirm.tr(context: context), + onPressed: () async { + Navigator.of(context).pop(); + LoaderBottomSheet.showLoader(); + emergencyServicesViewModel.clearRRTData(); + await emergencyServicesViewModel.getRRTProcedures(onSuccess: (val) { + LoaderBottomSheet.hideLoader(); + showCommonBottomSheetWithoutHeight( + padding: EdgeInsets.only(top: 24.h), + titleWidget: Transform.flip( + flipX: emergencyServicesViewModel.isArabic, + child: Utils.buildSvgWithAssets( + icon: AppAssets.arrow_back, + iconColor: Color(0xff2B353E), + fit: BoxFit.contain, + ), + ).onPress(() { + Navigator.pop(context); + }), + // title: "Rapid Response Team (RRT)".needTranslation, + context, + child: RrtRequestTypeSelect(), + isFullScreen: false, + isCloseButtonVisible: true, + hasBottomPadding: false, + backgroundColor: AppColors.bottomSheetBgColor, + callBackFunc: () {}, + ); + }); + }, + backgroundColor: Colors.white, + borderColor: Colors.white, + textColor: AppColors.primaryRedColor, + icon: AppAssets.checkmark_icon, + iconColor: AppColors.primaryRedColor, + ), + SizedBox(height: 8.h), + ], + ), ), ), - ), - isFullScreen: false, - isCloseButtonVisible: false, - hasBottomPadding: false, - backgroundColor: AppColors.primaryRedColor, - callBackFunc: () { - context.read().setTermsAndConditions(false); - }, - ); + isFullScreen: false, + isCloseButtonVisible: false, + hasBottomPadding: false, + backgroundColor: AppColors.primaryRedColor, + callBackFunc: () { + context.read().setTermsAndConditions(false); + }, + ); + } else { + Navigator.of(context).push( + CustomPageRoute( + page: ServiceInfoPage( + serviceName: LocaleKeys.emergencyServices.tr(), + serviceHeader: LocaleKeys.emergencyServiceHeader.tr(), + serviceDescription: LocaleKeys.emergencyServiceDescription.tr(), + serviceImage: AppAssets.emergencyService), + ), + ); + } }), ), SizedBox(height: 16.h), @@ -275,73 +342,87 @@ class EmergencyServicesPage extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ LocaleKeys.emergencyCheckIn.tr(context: context).toText16(isBold: true, color: AppColors.blackColor), - LocaleKeys.priorERCheckInToSkipLine.tr() - .toText12(color: AppColors.greyTextColor, fontWeight: FontWeight.w500), + LocaleKeys.priorERCheckInToSkipLine.tr().toText12(color: AppColors.greyTextColor, fontWeight: FontWeight.w500), ], ), ), SizedBox(width: 12.h), - Utils.buildSvgWithAssets(icon: AppAssets.forward_chevron_icon, width: 13.h, height: 13.h), + Transform.flip(flipX: appState.isArabic(), child: Utils.buildSvgWithAssets(icon: AppAssets.forward_chevron_icon, width: 13.h, height: 13.h)), ], ).onPress(() { - showCommonBottomSheetWithoutHeight( - context, - child: Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.primaryRedColor, borderRadius: 24.h), - child: Padding( - padding: EdgeInsets.all(24.h), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + if(appState.isAuthenticated) { + if (emergencyServicesViewModel.patientHasAdvanceERBalance) { + Navigator.of(context).push(CustomPageRoute(page: ErOnlineCheckinHome())); + } else { + showCommonBottomSheetWithoutHeight( + context, + child: Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.primaryRedColor, borderRadius: 24.h), + child: Padding( + padding: EdgeInsets.all(24.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - "".toText14(), - Utils.buildSvgWithAssets( - icon: AppAssets.cancel_circle_icon, - iconColor: AppColors.whiteColor, - width: 24.h, - height: 24.h, - fit: BoxFit.contain, - ).onPress(() { - Navigator.of(context).pop(); - }), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + "".toText14(), + Utils.buildSvgWithAssets( + icon: AppAssets.cancel_circle_icon, + iconColor: AppColors.whiteColor, + width: 24.h, + height: 24.h, + fit: BoxFit.contain, + ).onPress(() { + Navigator.of(context).pop(); + }), + ], + ), + Lottie.asset(AppAnimations.ambulanceAlert, repeat: false, reverse: false, frameRate: FrameRate(60), width: 120.h, height: 120.h, fit: BoxFit.contain), + SizedBox(height: 8.h), + LocaleKeys.confirm.tr().toText28(color: Colors.white, isBold: true), + SizedBox(height: 8.h), + LocaleKeys.areYouSureYouWantToMakeERCheckIn.tr().toText14(color: Colors.white, weight: FontWeight.w500), + SizedBox(height: 24.h), + CustomButton( + text: LocaleKeys.confirm.tr(context: context), + onPressed: () async { + Navigator.of(context).pop(); + LoaderBottomSheet.showLoader(loadingText: LocaleKeys.checkingYourERAppointmentStatus.tr()); + await context.read().checkPatientERAdvanceBalance(onSuccess: (dynamic response) { + LoaderBottomSheet.hideLoader(); + context.read().navigateToEROnlineCheckIn(); + }); + }, + backgroundColor: Colors.white, + borderColor: Colors.white, + textColor: AppColors.primaryRedColor, + icon: AppAssets.checkmark_icon, + iconColor: AppColors.primaryRedColor, + ), + SizedBox(height: 8.h), ], ), - Lottie.asset(AppAnimations.ambulanceAlert, - repeat: false, reverse: false, frameRate: FrameRate(60), width: 120.h, height: 120.h, fit: BoxFit.contain), - SizedBox(height: 8.h), - LocaleKeys.confirm.tr().toText28(color: Colors.white, isBold: true), - SizedBox(height: 8.h), - LocaleKeys.areYouSureYouWantToMakeERCheckIn.tr().toText14(color: Colors.white, weight: FontWeight.w500), - SizedBox(height: 24.h), - CustomButton( - text: LocaleKeys.confirm.tr(context: context), - onPressed: () async { - Navigator.of(context).pop(); - LoaderBottomSheet.showLoader(loadingText: LocaleKeys.checkingYourERAppointmentStatus.tr()); - await context.read().checkPatientERAdvanceBalance(onSuccess: (dynamic response) { - LoaderBottomSheet.hideLoader(); - context.read().navigateToEROnlineCheckIn(); - }); - }, - backgroundColor: Colors.white, - borderColor: Colors.white, - textColor: AppColors.primaryRedColor, - icon: AppAssets.checkmark_icon, - iconColor: AppColors.primaryRedColor, - ), - SizedBox(height: 8.h), - ], + ), ), + isFullScreen: false, + isCloseButtonVisible: false, + hasBottomPadding: false, + backgroundColor: AppColors.primaryRedColor, + callBackFunc: () {}, + ); + } + } else { + Navigator.of(context).push( + CustomPageRoute( + page: ServiceInfoPage( + serviceName: LocaleKeys.emergencyServices.tr(), + serviceHeader: LocaleKeys.emergencyServiceHeader.tr(), + serviceDescription: LocaleKeys.emergencyServiceDescription.tr(), + serviceImage: AppAssets.emergencyService), ), - ), - isFullScreen: false, - isCloseButtonVisible: false, - hasBottomPadding: false, - backgroundColor: AppColors.primaryRedColor, - callBackFunc: () {}, - ); + ); + } }), ), ], @@ -355,7 +436,7 @@ class EmergencyServicesPage extends StatelessWidget { onCloseClicked: () { context.read().flushPickupInformation(); }, - titleWidget: Transform.flip( + titleWidget: Transform.flip( flipX: emergencyServicesViewModel.isArabic, child: Utils.buildSvgWithAssets( icon: AppAssets.arrow_back, @@ -368,7 +449,42 @@ class EmergencyServicesPage extends StatelessWidget { openTranportationSelectionBottomSheet(context); }), context, - child: PickupLocation(onTap: () { + child: PickupLocation(onTap: (fromLocation) { + Navigator.of(context).pop(); + if(fromLocation == AmbulanceCallingPlace.TO_HOSPITAL){ + openNumberWayOfSelectionBottomSheet(context); + return; + } + context.read().flushSearchPredictions(); + context.read().navigateTOAmbulancePage(); + }), + isFullScreen: false, + isCloseButtonVisible: true, + hasBottomPadding: false, + backgroundColor: AppColors.bottomSheetBgColor, + callBackFunc: () {}, + ); + } + + openNumberWayOfSelectionBottomSheet(BuildContext context) { + showCommonBottomSheetWithoutHeight( + onCloseClicked: () { + context.read().flushPickupInformation(); + }, + titleWidget: Transform.flip( + flipX: emergencyServicesViewModel.isArabic, + child: Utils.buildSvgWithAssets( + icon: AppAssets.arrow_back, + iconColor: Color(0xff2B353E), + fit: BoxFit.contain, + ), + ).onPress(() { + context.read().flushPickupInformation(); + Navigator.pop(context); + openPickupDetailsBottomSheet(context); + }), + context, + child: WayPickup(onTap: () { Navigator.of(context).pop(); context.read().flushSearchPredictions(); context.read().navigateTOAmbulancePage(); diff --git a/lib/presentation/emergency_services/er_online_checkin/er_online_checkin_payment_details_page.dart b/lib/presentation/emergency_services/er_online_checkin/er_online_checkin_payment_details_page.dart index 463121d9..eee31da7 100644 --- a/lib/presentation/emergency_services/er_online_checkin/er_online_checkin_payment_details_page.dart +++ b/lib/presentation/emergency_services/er_online_checkin/er_online_checkin_payment_details_page.dart @@ -128,7 +128,7 @@ class ErOnlineCheckinPaymentDetailsPage extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ SizedBox( - width: 150.h, + width: 200.h, child: Utils.getPaymentMethods(), ), Row( diff --git a/lib/presentation/emergency_services/er_online_checkin/er_online_checkin_payment_page.dart b/lib/presentation/emergency_services/er_online_checkin/er_online_checkin_payment_page.dart index 8b9633c1..bc0e7344 100644 --- a/lib/presentation/emergency_services/er_online_checkin/er_online_checkin_payment_page.dart +++ b/lib/presentation/emergency_services/er_online_checkin/er_online_checkin_payment_page.dart @@ -468,12 +468,12 @@ class _ErOnlineCheckinPaymentPageState extends State context, child: Utils.getErrorWidget(loadingText: LocaleKeys.paymentFailedPleaseTryAgain.tr(context: context)), callBackFunc: () { - Navigator.pushAndRemoveUntil( - context, - CustomPageRoute( - page: LandingNavigation(), - ), - (r) => false); + // Navigator.pushAndRemoveUntil( + // context, + // CustomPageRoute( + // page: LandingNavigation(), + // ), + // (r) => false); }, isFullScreen: false, isCloseButtonVisible: true, diff --git a/lib/presentation/emergency_services/er_online_checkin/er_online_checkin_select_checkin_bottom_sheet.dart b/lib/presentation/emergency_services/er_online_checkin/er_online_checkin_select_checkin_bottom_sheet.dart index 3b2dc8ae..f4bbe3af 100644 --- a/lib/presentation/emergency_services/er_online_checkin/er_online_checkin_select_checkin_bottom_sheet.dart +++ b/lib/presentation/emergency_services/er_online_checkin/er_online_checkin_select_checkin_bottom_sheet.dart @@ -60,20 +60,20 @@ class ErOnlineCheckinSelectCheckinBottomSheet extends StatelessWidget { } }); }), - SizedBox(height: 16.h), - checkInOptionCard( - AppAssets.checkin_nfc_icon, - LocaleKeys.nfcNearFieldCommunication.tr(context: context), - LocaleKeys.scanPhoneViaNFC.tr(context: context), - ).onPress(() { - Future.delayed(const Duration(milliseconds: 500), () { - showNfcReader(context, onNcfScan: (String nfcId) { - Future.delayed(const Duration(milliseconds: 100), () { - sendCheckInRequest(nfcId, context); - }); - }, onCancel: () {}); - }); - }), + // SizedBox(height: 16.h), + // checkInOptionCard( + // AppAssets.checkin_nfc_icon, + // LocaleKeys.nfcNearFieldCommunication.tr(context: context), + // LocaleKeys.scanPhoneViaNFC.tr(context: context), + // ).onPress(() { + // Future.delayed(const Duration(milliseconds: 500), () { + // showNfcReader(context, onNcfScan: (String nfcId) { + // Future.delayed(const Duration(milliseconds: 100), () { + // sendCheckInRequest(nfcId, context); + // }); + // }, onCancel: () {}); + // }); + // }), SizedBox(height: 16.h), checkInOptionCard( AppAssets.checkin_qr_icon, diff --git a/lib/presentation/emergency_services/history/er_history_listing.dart b/lib/presentation/emergency_services/history/er_history_listing.dart index a60d4556..d72200c5 100644 --- a/lib/presentation/emergency_services/history/er_history_listing.dart +++ b/lib/presentation/emergency_services/history/er_history_listing.dart @@ -23,7 +23,6 @@ class ErHistoryListing extends StatelessWidget { return Scaffold( body: Column( children: [ - Expanded( child: CollapsingListView( title: LocaleKeys.history.tr(context: context), @@ -31,15 +30,12 @@ class ErHistoryListing extends StatelessWidget { physics: NeverScrollableScrollPhysics(), child: Column( children: [ - Selector( selector: (context, vm) => (vm.orderDisplayList, vm.historyLoading), builder: (context, data, _) { - return Column( children: [ orderChips(context, data.$2, data.$1), - Visibility( visible:data.$1.isNotEmpty == true, child: ListView.builder( diff --git a/lib/presentation/emergency_services/history/widget/ambulance_history_item.dart b/lib/presentation/emergency_services/history/widget/ambulance_history_item.dart index e1ebc83c..100c0cfa 100644 --- a/lib/presentation/emergency_services/history/widget/ambulance_history_item.dart +++ b/lib/presentation/emergency_services/history/widget/ambulance_history_item.dart @@ -13,6 +13,7 @@ import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; import 'package:provider/provider.dart'; +import 'dart:ui' as ui; import '../../../../core/utils/utils.dart'; @@ -37,11 +38,11 @@ class AmbulanceHistoryItem extends StatelessWidget { spacing: 8.h, children: [ RequestStatus(status: order.statusId ?? 0), - "Req ID: ${order.iD}".toText16(color: AppColors.textColor, weight: FontWeight.w600), + "Req ID: ${order.iD}".toText16(color: AppColors.textColor, weight: FontWeight.w600, isEnglishOnly: true), Row( spacing: 4.w, children: [ - chip( Utils.getDayMonthYearDateFormatted(DateUtil.convertStringToDate(order.time)), AppAssets.calendar, AppColors.blackBgColor), + chip(Utils.getDayMonthYearDateFormatted(DateUtil.convertStringToDate(order.time)), AppAssets.calendar, AppColors.blackBgColor), chip(LocaleKeys.ambulancerequest.tr(context: context), AppAssets.ambulance, AppColors.blackBgColor), ], ), @@ -71,11 +72,15 @@ class AmbulanceHistoryItem extends StatelessWidget { } chip(String title, String iconString, Color iconColor) { - return AppCustomChipWidget( - labelText: title, - icon: iconString, - iconColor: iconColor, - iconSize: 12.h, + return Directionality( + textDirection: ui.TextDirection.ltr, + child: AppCustomChipWidget( + labelText: title, + icon: iconString, + iconColor: iconColor, + iconSize: 12.h, + isEnglishOnly: true, + ), ); } diff --git a/lib/presentation/emergency_services/history/widget/rrt_item.dart b/lib/presentation/emergency_services/history/widget/rrt_item.dart index c8787791..8e910231 100644 --- a/lib/presentation/emergency_services/history/widget/rrt_item.dart +++ b/lib/presentation/emergency_services/history/widget/rrt_item.dart @@ -13,6 +13,7 @@ import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; import 'package:provider/provider.dart'; +import 'dart:ui' as ui; import '../../../../core/utils/utils.dart'; @@ -37,11 +38,11 @@ class RRTItem extends StatelessWidget { spacing: 8.h, children: [ RequestStatus(status: order.statusId ?? 0), - "Req ID: ${order.iD}".toText16(color: AppColors.textColor, weight: FontWeight.w600), + "Req ID: ${order.iD}".toText16(color: AppColors.textColor, weight: FontWeight.w600, isEnglishOnly: true), Row( spacing: 4.w, children: [ - chip( Utils.getDayMonthYearDateFormatted(DateUtil.convertStringToDate(order.time)), AppAssets.calendar, AppColors.blackBgColor), + chip(Utils.getDayMonthYearDateFormatted(DateUtil.convertStringToDate(order.time)), AppAssets.calendar, AppColors.blackBgColor), chip(LocaleKeys.rapidResponseTeam.tr(context: context), AppAssets.ic_rrt_vehicle, AppColors.blackBgColor), ], ), @@ -65,11 +66,15 @@ class RRTItem extends StatelessWidget { } chip(String title, String iconString, Color iconColor) { - return AppCustomChipWidget( - labelText: title, - icon: iconString, - iconColor: iconColor, - iconSize: 12.h, + return Directionality( + textDirection: ui.TextDirection.ltr, + child: AppCustomChipWidget( + labelText: title, + icon: iconString, + iconColor: iconColor, + iconSize: 12.h, + isEnglishOnly: true, + ), ); } diff --git a/lib/presentation/emergency_services/widgets/nearestERItem.dart b/lib/presentation/emergency_services/widgets/nearestERItem.dart index 0c4a5a6a..423b887e 100644 --- a/lib/presentation/emergency_services/widgets/nearestERItem.dart +++ b/lib/presentation/emergency_services/widgets/nearestERItem.dart @@ -13,6 +13,8 @@ import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; import 'package:provider/provider.dart'; +import 'dart:ui' as ui; + class NearestERItem extends StatelessWidget { final ProjectAvgERWaitingTime nearestERItem; final bool isLoading; @@ -68,20 +70,28 @@ class NearestERItem extends StatelessWidget { Row( spacing: 8.h, children: [ - AppCustomChipWidget( - labelText: "${nearestERItem.distanceInKilometers} km", - icon: AppAssets.location, - iconHasColor: false, - labelPadding: EdgeInsetsDirectional.only(start: 4.h, end: 0.h), - padding: EdgeInsets.all(8.h), - ).toShimmer2(isShow: isLoading), - AppCustomChipWidget( - labelText: "Expected waiting time: ${nearestERItem.getTime()} mins", - icon: AppAssets.waiting_time_clock, - iconHasColor: false, - labelPadding: EdgeInsetsDirectional.only(start: 4.h, end: 0.h), - padding: EdgeInsets.all(8.h), - ).toShimmer2(isShow: isLoading), + Directionality( + textDirection: ui.TextDirection.ltr, + child: AppCustomChipWidget( + labelText: "${nearestERItem.distanceInKilometers} km", + icon: AppAssets.location, + iconHasColor: false, + labelPadding: EdgeInsetsDirectional.only(start: 4.h, end: 0.h), + padding: EdgeInsets.all(8.h), + isEnglishOnly: true, + ).toShimmer2(isShow: isLoading), + ), + Directionality( + textDirection: ui.TextDirection.ltr, + child: AppCustomChipWidget( + labelText: "Expected waiting time: ${nearestERItem.getTime()} mins", + icon: AppAssets.waiting_time_clock, + iconHasColor: false, + labelPadding: EdgeInsetsDirectional.only(start: 4.h, end: 0.h), + padding: EdgeInsets.all(8.h), + isEnglishOnly: true, + ).toShimmer2(isShow: isLoading), + ), ], ), SizedBox(height: 16.h), diff --git a/lib/presentation/habib_wallet/habib_wallet_page.dart b/lib/presentation/habib_wallet/habib_wallet_page.dart index 2a3da660..2cdfc8b6 100644 --- a/lib/presentation/habib_wallet/habib_wallet_page.dart +++ b/lib/presentation/habib_wallet/habib_wallet_page.dart @@ -74,7 +74,7 @@ class _HabibWalletState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ "${_appState.getAuthenticatedUser()!.firstName!} ${_appState.getAuthenticatedUser()!.lastName!}".toText19(isBold: true, color: Colors.white), - "MRN: ${_appState.getAuthenticatedUser()!.patientId!}".toText14(weight: FontWeight.w500, color: AppColors.greyTextColor), + "MRN: ${_appState.getAuthenticatedUser()!.patientId!}".toText14(weight: FontWeight.w500, color: AppColors.greyTextColor, isEnglishOnly: true), ], ).expanded, Utils.buildSvgWithAssets(icon: AppAssets.habiblogo, width: 24.h, height: 24.h, applyThemeColor: false), @@ -84,7 +84,8 @@ class _HabibWalletState extends State { LocaleKeys.balanceAmount.tr(context: context).toText14(weight: FontWeight.w500, color: Colors.white), SizedBox(height: 4.h), Consumer(builder: (context, habibWalletVM, child) { - return Utils.getPaymentAmountWithSymbol2(habibWalletVM.habibWalletAmount, textColor: Colors.white, iconColor: Colors.white, iconSize: 16, isExpanded: false) + return Utils.getPaymentAmountWithSymbol2(num.parse(NumberFormat.decimalPattern().format(habibWalletVM.habibWalletAmount)), + textColor: Colors.white, iconColor: Colors.white, iconSize: 16, isExpanded: false) .toShimmer2(isShow: habibWalletVM.isWalletAmountLoading, radius: 12.h, width: 80.h, height: 24.h); }), ], @@ -138,7 +139,8 @@ class _HabibWalletState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded(child: habibWalletVM.habibWalletBalanceList[index].projectDescription!.toText16(weight: FontWeight.w500, color: AppColors.textColor)), - Utils.getPaymentAmountWithSymbol2(habibWalletVM.habibWalletBalanceList[index].patientAdvanceBalanceAmount!, textColor: AppColors.textColor, iconColor: AppColors.textColor, iconSize: 18.h, isExpanded: false, fontSize: 28.f, fontWeight: FontWeight.w500), + Utils.getPaymentAmountWithSymbol2(num.parse(NumberFormat.decimalPattern().format(habibWalletVM.habibWalletBalanceList[index].patientAdvanceBalanceAmount!)), + textColor: AppColors.textColor, iconColor: AppColors.textColor, iconSize: 18.h, isExpanded: false, fontSize: 28.f, fontWeight: FontWeight.w500), ], ).paddingSymmetrical(0, 12.h); }, diff --git a/lib/presentation/habib_wallet/recharge_wallet_page.dart b/lib/presentation/habib_wallet/recharge_wallet_page.dart index ef1642eb..74c0a69d 100644 --- a/lib/presentation/habib_wallet/recharge_wallet_page.dart +++ b/lib/presentation/habib_wallet/recharge_wallet_page.dart @@ -140,7 +140,7 @@ class _RechargeWalletPageState extends State { children: [ (habibWalletVM.getSelectedRechargeTypeValue()).toText12(color: AppColors.greyTextColor, fontWeight: FontWeight.w500), "${LocaleKeys.medicalFile.tr(context: context)}: ${habibWalletVM.fileNumber}" - .toText14(color: AppColors.textColor, weight: FontWeight.w500, letterSpacing: -0.2), + .toText14(color: AppColors.textColor, weight: FontWeight.w500, letterSpacing: -0.2, isEnglishOnly: true), ], ), ], @@ -203,7 +203,7 @@ class _RechargeWalletPageState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ LocaleKeys.email.tr(context: context).toText12(color: AppColors.greyTextColor, fontWeight: FontWeight.w500), - "${appState.getAuthenticatedUser()!.emailAddress}".toText14(color: AppColors.textColor, weight: FontWeight.w500, letterSpacing: -0.2), + "${appState.getAuthenticatedUser()!.emailAddress}".toText14(color: AppColors.textColor, weight: FontWeight.w500, letterSpacing: -0.2, isEnglishOnly: true), ], ), ], diff --git a/lib/presentation/habib_wallet/wallet_payment_confirm_page.dart b/lib/presentation/habib_wallet/wallet_payment_confirm_page.dart index 9efcb01b..151b3f21 100644 --- a/lib/presentation/habib_wallet/wallet_payment_confirm_page.dart +++ b/lib/presentation/habib_wallet/wallet_payment_confirm_page.dart @@ -62,7 +62,7 @@ class _WalletPaymentConfirmPageState extends State { children: [ Expanded( child: CollapsingListView( - title: "Select Payment Method", + title: LocaleKeys.selectPaymentOption.tr(context: context), child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -169,9 +169,17 @@ class _WalletPaymentConfirmPageState extends State { spacing: 4.h, runSpacing: 4.h, children: [ - AppCustomChipWidget(labelText: "${LocaleKeys.fileno.tr(context: context)}.: ${habibWalletVM.fileNumber}"), - AppCustomChipWidget(labelText: "${LocaleKeys.mobileNumber.tr(context: context)}: ${habibWalletVM.mobileNumber}"), - AppCustomChipWidget(labelText: "${habibWalletVM.selectedHospital!.name}"), + AppCustomChipWidget( + labelText: "${LocaleKeys.fileno.tr(context: context)}.: ${habibWalletVM.fileNumber}", + isEnglishOnly: true, + ), + AppCustomChipWidget( + labelText: "${LocaleKeys.mobileNumber.tr(context: context)}: ${habibWalletVM.mobileNumber}", + isEnglishOnly: true, + ), + AppCustomChipWidget( + labelText: "${habibWalletVM.selectedHospital!.name}", + ), ], ).paddingSymmetrical(24.h, 0.h), SizedBox(height: 16.h), @@ -181,7 +189,9 @@ class _WalletPaymentConfirmPageState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ LocaleKeys.totalAmountToPay.tr(context: context).toText16(isBold: true), - Utils.getPaymentAmountWithSymbol(habibWalletVM.walletRechargeAmount.toString().toText24(isBold: true), AppColors.blackColor, 15.h, isSaudiCurrency: true), + Utils.getPaymentAmountWithSymbol( + NumberFormat.decimalPattern().format(habibWalletVM.walletRechargeAmount).toString().toText24(isBold: true, isEnglishOnly: true), AppColors.blackColor, 15.h, + isSaudiCurrency: true), ], ).paddingSymmetrical(24.h, 0.h), SizedBox(height: 12.h), @@ -320,6 +330,7 @@ class _WalletPaymentConfirmPageState extends State { }, isFullScreen: false, isCloseButtonVisible: true, + isAutoDismiss: true ); }, onError: (err) { diff --git a/lib/presentation/health_calculators_and_converts/health_calculators_page.dart b/lib/presentation/health_calculators_and_converts/health_calculators_page.dart index 4f519d66..0b844b4f 100644 --- a/lib/presentation/health_calculators_and_converts/health_calculators_page.dart +++ b/lib/presentation/health_calculators_and_converts/health_calculators_page.dart @@ -1,6 +1,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/enums.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; @@ -57,7 +58,9 @@ class _HealthCalculatorsPageState extends State { SizedBox( width: 12.w, ), - Utils.buildSvgWithAssets(icon: AppAssets.arrowRight, width: 24.w, height: 24.h, fit: BoxFit.contain, iconColor: AppColors.textColor), + Transform.flip( + flipX: getIt.get().isArabic(), + child: Utils.buildSvgWithAssets(icon: AppAssets.arrowRight, width: 24.w, height: 24.h, fit: BoxFit.contain, iconColor: AppColors.textColor)), ], ).paddingAll(16.w)) .onPress(() { @@ -86,7 +89,9 @@ class _HealthCalculatorsPageState extends State { ), ), SizedBox(width: 12.w), - Utils.buildSvgWithAssets(icon: AppAssets.arrowRight, width: 24.w, height: 24.h, fit: BoxFit.contain, iconColor: AppColors.textColor), + Transform.flip( + flipX: getIt.get().isArabic(), + child: Utils.buildSvgWithAssets(icon: AppAssets.arrowRight, width: 24.w, height: 24.h, fit: BoxFit.contain, iconColor: AppColors.textColor)), ], ).paddingAll(16.w)) .onPress(() { diff --git a/lib/presentation/health_trackers/health_trackers_page.dart b/lib/presentation/health_trackers/health_trackers_page.dart index 66630f28..ae57f7db 100644 --- a/lib/presentation/health_trackers/health_trackers_page.dart +++ b/lib/presentation/health_trackers/health_trackers_page.dart @@ -2,6 +2,8 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/enums.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/route_extensions.dart'; @@ -56,12 +58,15 @@ Widget buildHealthTrackerCard({ ), ), SizedBox(width: 12.w), - Utils.buildSvgWithAssets( - icon: AppAssets.arrowRight, - width: 24.w, - height: 24.h, - fit: BoxFit.contain, - iconColor: AppColors.textColor, + Transform.flip( + flipX: getIt.get().isArabic(), + child: Utils.buildSvgWithAssets( + icon: AppAssets.arrowRight, + width: 24.w, + height: 24.h, + fit: BoxFit.contain, + iconColor: AppColors.textColor, + ), ), ], ).paddingAll(16.w), diff --git a/lib/presentation/hmg_services/services_page.dart b/lib/presentation/hmg_services/services_page.dart index 4996850b..d2f42f52 100644 --- a/lib/presentation/hmg_services/services_page.dart +++ b/lib/presentation/hmg_services/services_page.dart @@ -12,6 +12,7 @@ import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/authentication/authentication_view_model.dart'; import 'package:hmg_patient_app_new/features/blood_donation/blood_donation_view_model.dart'; +import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart'; import 'package:hmg_patient_app_new/features/emergency_services/emergency_services_view_model.dart'; import 'package:hmg_patient_app_new/features/habib_wallet/habib_wallet_view_model.dart'; import 'package:hmg_patient_app_new/features/hmg_services/models/ui_models/hmg_services_component_model.dart'; @@ -20,12 +21,14 @@ import 'package:hmg_patient_app_new/features/medical_file/medical_file_view_mode import 'package:hmg_patient_app_new/features/water_monitor/water_monitor_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/blood_donation/blood_donation_page.dart'; +import 'package:hmg_patient_app_new/presentation/book_appointment/book_appointment_page.dart'; import 'package:hmg_patient_app_new/presentation/contact_us/contact_us.dart'; import 'package:hmg_patient_app_new/presentation/emergency_services/emergency_services_page.dart'; import 'package:hmg_patient_app_new/presentation/habib_wallet/habib_wallet_page.dart'; import 'package:hmg_patient_app_new/presentation/habib_wallet/recharge_wallet_page.dart'; import 'package:hmg_patient_app_new/presentation/hmg_services/services_view.dart'; import 'package:hmg_patient_app_new/presentation/home/data/landing_page_data.dart'; +import 'package:hmg_patient_app_new/presentation/home/service_info_page.dart'; import 'package:hmg_patient_app_new/presentation/home/widgets/large_service_card.dart'; import 'package:hmg_patient_app_new/presentation/medical_file/medical_file_page.dart'; import 'package:hmg_patient_app_new/presentation/parking/paking_page.dart'; @@ -54,23 +57,23 @@ class ServicesPage extends StatelessWidget { late final List hmgServices = [ HmgServicesComponentModel(11, LocaleKeys.emergencyServices.tr(), "", AppAssets.emergency_services_icon, bgColor: AppColors.primaryRedColor, true, route: null, onTap: () async { - if (getIt.get().isAuthenticated) { - getIt.get().flushData(); - getIt.get().getTransportationOrders( - showLoader: false, - ); - getIt.get().getRRTOrders( - showLoader: false, - ); + // if (getIt.get().isAuthenticated) { + // getIt.get().flushData(); + // getIt.get().getTransportationOrders( + // showLoader: false, + // ); + // getIt.get().getRRTOrders( + // showLoader: false, + // ); Navigator.of(getIt.get().navigatorKey.currentContext!).push( CustomPageRoute( page: EmergencyServicesPage(), settings: const RouteSettings(name: '/EmergencyServicesPage'), ), ); - } else { - await getIt.get().onLoginPressed(); - } + // } else { + // await getIt.get().onLoginPressed(); + // } }), HmgServicesComponentModel( 11, @@ -79,13 +82,25 @@ class ServicesPage extends StatelessWidget { AppAssets.appointment_calendar_icon, bgColor: AppColors.bookAppointment, true, - route: AppRoutes.bookAppointmentPage, + route: null, + onTap: () { + getIt.get().onTabChanged(0); + Navigator.of(getIt().navigatorKey.currentContext!).push(CustomPageRoute(page: BookAppointmentPage())); + } ), HmgServicesComponentModel(5, LocaleKeys.completeCheckup.tr(), "", AppAssets.comprehensiveCheckup, bgColor: AppColors.bgGreenColor, true, route: null, onTap: () async { if (getIt.get().isAuthenticated) { getIt.get().pushPageRoute(AppRoutes.comprehensiveCheckupPage); } else { - await getIt.get().onLoginPressed(); + Navigator.of(getIt().navigatorKey.currentContext!).push( + CustomPageRoute( + page: ServiceInfoPage( + serviceName: LocaleKeys.comprehensiveMedicalCheckup.tr(), + serviceHeader: LocaleKeys.cmcServiceHeader.tr(), + serviceDescription: LocaleKeys.cmcServiceDescription.tr(), + serviceImage: AppAssets.cmcService), + ), + ); } }), HmgServicesComponentModel( @@ -137,7 +152,15 @@ class ServicesPage extends StatelessWidget { if (getIt.get().isAuthenticated) { getIt.get().pushPageRoute(AppRoutes.eReferralPage); } else { - await getIt.get().onLoginPressed(); + Navigator.of(getIt().navigatorKey.currentContext!).push( + CustomPageRoute( + page: ServiceInfoPage( + serviceName: LocaleKeys.eReferralServices.tr(), + serviceHeader: LocaleKeys.eReferralServiceHeader.tr(), + serviceDescription: LocaleKeys.eReferralServiceDescription.tr(), + serviceImage: AppAssets.eReferralService), + ), + ); } }), HmgServicesComponentModel( @@ -147,7 +170,26 @@ class ServicesPage extends StatelessWidget { AppAssets.blood_donation_icon, bgColor: AppColors.bloodDonationCardColor, true, - route: AppRoutes.bloodDonationPage, + route: null, + onTap: () { + if (getIt.get().isAuthenticated) { + Navigator.of(getIt().navigatorKey.currentContext!).push( + CustomPageRoute( + page: BloodDonationPage(), + ), + ); + } else { + Navigator.of(getIt().navigatorKey.currentContext!).push( + CustomPageRoute( + page: ServiceInfoPage( + serviceName: LocaleKeys.bloodDonation.tr(), + serviceHeader: LocaleKeys.bloodDonationServiceHeader.tr(), + serviceDescription: LocaleKeys.bloodDonationServiceDescription.tr(), + serviceImage: AppAssets.bloodDonationService), + ), + ); + } + } ), HmgServicesComponentModel( 113, @@ -230,7 +272,15 @@ class ServicesPage extends StatelessWidget { if (getIt.get().isAuthenticated) { getIt.get().pushPageRoute(AppRoutes.healthTrackersPage); } else { - await getIt.get().onLoginPressed(); + Navigator.of(getIt().navigatorKey.currentContext!).push( + CustomPageRoute( + page: ServiceInfoPage( + serviceName: LocaleKeys.healthTrackers.tr(), + serviceHeader: LocaleKeys.healthTrackersServiceHeader.tr(), + serviceDescription: LocaleKeys.healthTrackersServiceDescription.tr(), + serviceImage: AppAssets.healthTrackersService), + ), + ); } }, ), @@ -263,7 +313,15 @@ class ServicesPage extends StatelessWidget { }, ); } else { - await getIt.get().onLoginPressed(); + Navigator.of(getIt().navigatorKey.currentContext!).push( + CustomPageRoute( + page: ServiceInfoPage( + serviceName: LocaleKeys.waterConsumption.tr(), + serviceHeader: LocaleKeys.waterConsumptionServiceHeader.tr(), + serviceDescription: LocaleKeys.waterConsumptionServiceDescription.tr(), + serviceImage: AppAssets.waterConsumptionService), + ), + ); } }, ), @@ -297,8 +355,21 @@ class ServicesPage extends StatelessWidget { if (getIt.get().isAuthenticated) { getIt.get().pushPageRoute(AppRoutes.smartWatches); } else { - await getIt.get().onLoginPressed(); + Navigator.of(getIt().navigatorKey.currentContext!).push( + CustomPageRoute( + page: ServiceInfoPage( + serviceName: LocaleKeys.smartWatches.tr(), + serviceHeader: LocaleKeys.smartWatchServiceHeader.tr(), + serviceDescription: LocaleKeys.smartWatchServiceDescription.tr(), + serviceImage: AppAssets.smartWatchService), + ), + ); } + // if (getIt.get().isAuthenticated) { + // getIt.get().pushPageRoute(AppRoutes.smartWatches); + // } else { + // await getIt.get().onLoginPressed(); + // } }, // route: AppRoutes.huaweiHealthExample, ), @@ -368,168 +439,173 @@ class ServicesPage extends StatelessWidget { ), ), SizedBox(height: 24.h), - LocaleKeys.personalServices.tr().toText18(weight: FontWeight.w600).paddingSymmetrical(24.w, 0), - SizedBox(height: 16.h), - Row( + getIt.get().isAuthenticated ? Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Expanded( - child: Container( - height: 183.h, - width: 183.h, - padding: EdgeInsets.all(16.w), - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 20.r, - hasShadow: false, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - spacing: 8.w, - crossAxisAlignment: CrossAxisAlignment.center, + LocaleKeys.personalServices.tr().toText18(weight: FontWeight.w600).paddingSymmetrical(24.w, 0), + SizedBox(height: 16.h), + Row( + children: [ + Expanded( + child: Container( + height: 183.h, + width: 183.h, + padding: EdgeInsets.all(16.w), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.r, + hasShadow: false, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Utils.buildSvgWithAssets(icon: AppAssets.wallet, width: 40.w, height: 40.h, applyThemeColor: false), - LocaleKeys.habibWallet.tr().toText14(weight: FontWeight.w600, maxlines: 2).expanded, - Utils.buildSvgWithAssets(icon: getIt.get().isArabic() ? AppAssets.arrow_back : AppAssets.arrow_forward), + Row( + spacing: 8.w, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.wallet, width: 40.w, height: 40.h, applyThemeColor: false), + LocaleKeys.habibWallet.tr().toText14(weight: FontWeight.w600, maxlines: 2).expanded, + Utils.buildSvgWithAssets(icon: getIt.get().isArabic() ? AppAssets.arrow_back : AppAssets.arrow_forward), + ], + ), + Spacer(), + getIt.get().isAuthenticated + ? Consumer(builder: (context, habibWalletVM, child) { + return Utils.getPaymentAmountWithSymbol2(num.parse(NumberFormat.decimalPattern().format(habibWalletVM.habibWalletAmount)), isExpanded: false) + .toShimmer2(isShow: habibWalletVM.isWalletAmountLoading, radius: 12.r, width: 80.w, height: 24.h); + }) + : LocaleKeys.loginToViewWalletBalance.tr().toText12(fontWeight: FontWeight.w500, maxLine: 2), + Spacer(), + getIt.get().isAuthenticated + ? CustomButton( + height: 40.h, + icon: AppAssets.recharge_icon, + iconSize: 24.w, + iconColor: AppColors.infoColor, + textColor: AppColors.infoColor, + text: LocaleKeys.recharge.tr(), + borderWidth: 0.w, + fontWeight: FontWeight.w500, + borderColor: Colors.transparent, + backgroundColor: Color(0xff45A2F8).withValues(alpha: 0.08), + padding: EdgeInsets.all(8.w), + fontSize: 14.f, + onPressed: () { + Navigator.of(context).push(CustomPageRoute(page: RechargeWalletPage())); + }, + ) + : SizedBox.shrink(), ], - ), - Spacer(), - getIt.get().isAuthenticated - ? Consumer(builder: (context, habibWalletVM, child) { - return Utils.getPaymentAmountWithSymbol2(habibWalletVM.habibWalletAmount, isExpanded: false) - .toShimmer2(isShow: habibWalletVM.isWalletAmountLoading, radius: 12.r, width: 80.w, height: 24.h); - }) - : LocaleKeys.loginToViewWalletBalance.tr().toText12(fontWeight: FontWeight.w500, maxLine: 2), - Spacer(), - getIt.get().isAuthenticated - ? CustomButton( - height: 40.h, - icon: AppAssets.recharge_icon, - iconSize: 24.w, - iconColor: AppColors.infoColor, - textColor: AppColors.infoColor, - text: LocaleKeys.recharge.tr(), - borderWidth: 0.w, - fontWeight: FontWeight.w500, - borderColor: Colors.transparent, - backgroundColor: Color(0xff45A2F8).withValues(alpha: 0.08), - padding: EdgeInsets.all(8.w), - fontSize: 14.f, - onPressed: () { - Navigator.of(context).push(CustomPageRoute(page: RechargeWalletPage())); - }, - ) - : SizedBox.shrink(), - ], - ).onPress(() async { - if (getIt.get().isAuthenticated) { - Navigator.of(context).push(CustomPageRoute(page: HabibWalletPage())); - } else { - await getIt.get().onLoginPressed(); - } - }), - ), - ), - SizedBox(width: 16.w), - Expanded( - child: Container( - height: 183.h, - width: 183.h, - padding: EdgeInsets.all(16.w), - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 20.r, - hasShadow: false, + ).onPress(() async { + if (getIt.get().isAuthenticated) { + Navigator.of(context).push(CustomPageRoute(page: HabibWalletPage())); + } else { + await getIt.get().onLoginPressed(); + } + }), + ), ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - spacing: 8.w, - crossAxisAlignment: CrossAxisAlignment.center, + SizedBox(width: 16.w), + Expanded( + child: Container( + height: 183.h, + width: 183.h, + padding: EdgeInsets.all(16.w), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.r, + hasShadow: false, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Utils.buildSvgWithAssets(icon: AppAssets.services_medical_file_icon, width: 40.w, height: 40.h, applyThemeColor: false), - LocaleKeys.medicalFile.tr().toText16(weight: FontWeight.w600, maxlines: 2).expanded, - Utils.buildSvgWithAssets(icon: getIt.get().isArabic() ? AppAssets.arrow_back : AppAssets.arrow_forward), + Row( + spacing: 8.w, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.services_medical_file_icon, width: 40.w, height: 40.h, applyThemeColor: false), + LocaleKeys.medicalFile.tr().toText16(weight: FontWeight.w600, maxlines: 2).expanded, + Utils.buildSvgWithAssets(icon: getIt.get().isArabic() ? AppAssets.arrow_back : AppAssets.arrow_forward), + ], + ), + Spacer(), + getIt.get().isAuthenticated + ? Wrap( + spacing: -12.h, + // runSpacing: 0.h, + children: [ + Utils.buildImgWithAssets( + icon: AppAssets.babyGirlImg, + height: 32.h, + width: 32.w, + border: 1, + fit: BoxFit.contain, + borderRadius: 50.r, + ), + Utils.buildImgWithAssets( + icon: AppAssets.femaleImg, + height: 32.h, + width: 32.w, + border: 1, + borderRadius: 50.r, + fit: BoxFit.contain, + ), + Utils.buildImgWithAssets( + icon: AppAssets.maleImg, + height: 32.h, + width: 32.w, + border: 1, + borderRadius: 50.r, + fit: BoxFit.contain, + ), + ], + ) + : LocaleKeys.loginToViewMedicalFile.tr().toText12(fontWeight: FontWeight.w500, maxLine: 2), + Spacer(), + getIt.get().isAuthenticated + ? CustomButton( + height: 40.h, + icon: AppAssets.add_icon, + iconSize: 24.h, + iconColor: AppColors.primaryRedColor, + textColor: AppColors.primaryRedColor, + text: LocaleKeys.addMember.tr(), + borderWidth: 0.w, + fontWeight: FontWeight.w500, + borderColor: Colors.transparent, + backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.08), + padding: EdgeInsets.all(8.w), + fontSize: 14.f, + onPressed: () { + DialogService dialogService = getIt.get(); + medicalFileViewModel.clearAuthValues(); + dialogService.showAddFamilyFileSheet( + label: LocaleKeys.addFamilyMember.tr(), + message: LocaleKeys.pleaseFillBelowFieldToAddNewFamilyMember.tr(), + onVerificationPress: () { + medicalFileViewModel.addFamilyFile(otpTypeEnum: OTPTypeEnum.sms); + }); + }, + ) + : SizedBox.shrink(), ], - ), - Spacer(), - getIt.get().isAuthenticated - ? Wrap( - spacing: -12.h, - // runSpacing: 0.h, - children: [ - Utils.buildImgWithAssets( - icon: AppAssets.babyGirlImg, - height: 32.h, - width: 32.w, - border: 1, - fit: BoxFit.contain, - borderRadius: 50.r, - ), - Utils.buildImgWithAssets( - icon: AppAssets.femaleImg, - height: 32.h, - width: 32.w, - border: 1, - borderRadius: 50.r, - fit: BoxFit.contain, - ), - Utils.buildImgWithAssets( - icon: AppAssets.maleImg, - height: 32.h, - width: 32.w, - border: 1, - borderRadius: 50.r, - fit: BoxFit.contain, - ), - ], - ) - : LocaleKeys.loginToViewMedicalFile.tr().toText12(fontWeight: FontWeight.w500, maxLine: 2), - Spacer(), - getIt.get().isAuthenticated - ? CustomButton( - height: 40.h, - icon: AppAssets.add_icon, - iconSize: 24.h, - iconColor: AppColors.primaryRedColor, - textColor: AppColors.primaryRedColor, - text: LocaleKeys.addMember.tr(), - borderWidth: 0.w, - fontWeight: FontWeight.w500, - borderColor: Colors.transparent, - backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.08), - padding: EdgeInsets.all(8.w), - fontSize: 14.f, - onPressed: () { - DialogService dialogService = getIt.get(); - medicalFileViewModel.clearAuthValues(); - dialogService.showAddFamilyFileSheet( - label: LocaleKeys.addFamilyMember.tr(), - message: LocaleKeys.pleaseFillBelowFieldToAddNewFamilyMember.tr(), - onVerificationPress: () { - medicalFileViewModel.addFamilyFile(otpTypeEnum: OTPTypeEnum.sms); - }); - }, - ) - : SizedBox.shrink(), - ], - ).onPress(() async { - if (getIt.get().isAuthenticated) { - Navigator.of(context).push( - CustomPageRoute( - page: MedicalFilePage(), - ), - ); - } else { - await getIt.get().onLoginPressed(); - } - }), - ), - ), + ).onPress(() async { + if (getIt.get().isAuthenticated) { + Navigator.of(context).push( + CustomPageRoute( + page: MedicalFilePage(), + ), + ); + } else { + await getIt.get().onLoginPressed(); + } + }), + ), + ), + ], + ).paddingSymmetrical(24.w, 0), ], - ).paddingSymmetrical(24.w, 0), + ) : SizedBox(), SizedBox(height: 24.h), LocaleKeys.healthTools.tr().toText18(weight: FontWeight.w600).paddingSymmetrical(24.w, 0), SizedBox(height: 16.h), diff --git a/lib/presentation/home/data/landing_page_data.dart b/lib/presentation/home/data/landing_page_data.dart index 4606c800..33b12855 100644 --- a/lib/presentation/home/data/landing_page_data.dart +++ b/lib/presentation/home/data/landing_page_data.dart @@ -1,6 +1,8 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/home/data/service_card_data.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; @@ -123,8 +125,8 @@ class LandingPageData { ServiceCardData( serviceName: "my_doctors", icon: AppAssets.my_doctors_icon, - title: "My", - subtitle: "Doctors", + title: getIt.get().isArabic() ? LocaleKeys.myDoctor : "My", + subtitle: getIt.get().isArabic() ? "" : "Doctors", backgroundColor: AppColors.whiteColor, iconColor: AppColors.blackColor, textColor: AppColors.blackColor, diff --git a/lib/presentation/home/landing_page.dart b/lib/presentation/home/landing_page.dart index 14684c0c..32525234 100644 --- a/lib/presentation/home/landing_page.dart +++ b/lib/presentation/home/landing_page.dart @@ -133,22 +133,22 @@ class _LandingPageState extends State { // Commented as per new requirement to remove rating popup from the app - // if(!appState.isRatedVisible) { - // appointmentRatingViewModel.getLastRatingAppointment(onSuccess: (response) { - // if (appointmentRatingViewModel.appointmentRatedList.isNotEmpty) { - // appointmentRatingViewModel.getAppointmentDetails(appointmentRatingViewModel.appointmentRatedList.last.appointmentNo!, appointmentRatingViewModel.appointmentRatedList.last.projectID!, - // onSuccess: ((response) { - // appointmentRatingViewModel.setClinicOrDoctor(false); - // appointmentRatingViewModel.setTitle("Rate Doctor".needTranslation); - // appointmentRatingViewModel.setSubTitle("How was your last visit with doctor?".needTranslation); - // openLastRating(); - // appState.setRatedVisible(true); - // }), - // ); - // } - // }, - // ); - // } + if(!appState.isRatedVisible) { + appointmentRatingViewModel.getLastRatingAppointment(onSuccess: (response) { + if (appointmentRatingViewModel.appointmentRatedList.isNotEmpty) { + appointmentRatingViewModel.getAppointmentDetails(appointmentRatingViewModel.appointmentRatedList.last.appointmentNo!, appointmentRatingViewModel.appointmentRatedList.last.projectID!, + onSuccess: ((response) { + appointmentRatingViewModel.setClinicOrDoctor(false); + appointmentRatingViewModel.setTitle(LocaleKeys.rateDoctor.tr(context: context)); + appointmentRatingViewModel.setSubTitle(LocaleKeys.howWasYourLastVisitWithDoctor.tr(context: context)); + openLastRating(); + appState.setRatedVisible(true); + }), + ); + } + }, + ); + } } }); super.initState(); @@ -173,7 +173,7 @@ class _LandingPageState extends State { children: [ SingleChildScrollView( padding: EdgeInsets.only( - top: (!insuranceVM.isInsuranceLoading && insuranceVM.isInsuranceExpired && insuranceVM.isInsuranceExpiryBannerShown) + top: (appState.isAuthenticated && !insuranceVM.isInsuranceLoading && insuranceVM.isInsuranceExpired && insuranceVM.isInsuranceExpiryBannerShown) ? (MediaQuery.paddingOf(context).top + 70.h) : kToolbarHeight + 0.h, bottom: 24), @@ -199,6 +199,7 @@ class _LandingPageState extends State { Navigator.of(context).push( CustomPageRoute( + direction: AxisDirection.down, page: FamilyMedicalScreen(), ), ); @@ -234,8 +235,8 @@ class _LandingPageState extends State { mainAxisSize: MainAxisSize.min, spacing: 18.h, children: [ - Stack(children: [ - if (appState.isAuthenticated) + Stack(clipBehavior: Clip.none, children: [ + if (appState.isAuthenticated) Utils.buildSvgWithAssets(icon: AppAssets.bell, height: 24.h, width: 24.h).onPress(() async { if (appState.isAuthenticated) { notificationsViewModel.setNotificationStatusID(2); @@ -252,22 +253,22 @@ class _LandingPageState extends State { }), (appState.isAuthenticated && (int.parse(todoSectionVM.notificationsCount ?? "0") > 0)) ? Positioned( - right: 0, - top: 0, + right: appState.isArabic() ? 8.w : -8.w, + top: -8.h, + // left: 4.h, + // bottom: 4.h, child: Container( - width: 8.w, - height: 8.h, - padding: EdgeInsets.all(4), + width: 18.w, + height: 18.h, + padding: EdgeInsets.all(2), decoration: BoxDecoration( color: AppColors.primaryRedColor, borderRadius: BorderRadius.circular(20.r), ), child: Text( - "", + todoSectionVM.notificationsCount.toString(), style: TextStyle( - color: Colors.white, - fontSize: 8.f, - ), + color: Colors.white, fontFamily: "Poppins", fontSize: 10.f, fontWeight: FontWeight.w500), textAlign: TextAlign.center, ), ), @@ -349,7 +350,7 @@ class _LandingPageState extends State { ).paddingSymmetrical(24.w, 0.h) : SizedBox.shrink(), appState.isAuthenticated - ? Column( + ? Column( children: [ SizedBox(height: 12.h), Row( @@ -365,6 +366,8 @@ class _LandingPageState extends State { ), ], ).paddingSymmetrical(24.h, 0.h).onPress(() { + myAppointmentsViewModel.onTabChange(0); + myAppointmentsViewModel.updateListWRTTab(0); Navigator.of(context).push(CustomPageRoute(page: MyAppointmentsPage())); }), Consumer3( @@ -498,67 +501,67 @@ class _LandingPageState extends State { ), // Consumer for ER Online Check-In pending request - Consumer( - builder: (context, emergencyServicesVM, child) { - return emergencyServicesVM.patientHasAdvanceERBalance - ? Column( - children: [ - SizedBox(height: 16.h), - Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 20.r, - hasShadow: false, - side: BorderSide(color: AppColors.primaryRedColor, width: 3.h), - ), - width: double.infinity, - child: Padding( - padding: EdgeInsets.all(16.h), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Row( - // mainAxisAlignment: MainAxisAlignment.spaceBetween, - // children: [ - // AppCustomChipWidget( - // labelText: LocaleKeys.erOnlineCheckInRequest.tr(context: context), - // backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.10), - // textColor: AppColors.primaryRedColor, - // ), - // Utils.buildSvgWithAssets(icon: AppAssets.appointment_checkin_icon, width: 24.h, height: 24.h, iconColor: AppColors.primaryRedColor), - // ], - // ), - SizedBox(height: 8.h), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - LocaleKeys.youHaveEROnlineCheckInRequest.tr(context: context).toText12(isBold: true), - Transform.flip( - flipX: getIt.get().isArabic(), - child: Utils.buildSvgWithAssets( - icon: AppAssets.forward_arrow_icon_small, - iconColor: AppColors.blackColor, - width: 20.h, - height: 15.h, - fit: BoxFit.contain, - ), - ), - ], - ), - ], - ), - ), - ).paddingSymmetrical(24.h, 0.h).onPress(() { - Navigator.of(context).push(CustomPageRoute(page: ErOnlineCheckinHome())); - // context.read().navigateToEROnlineCheckIn(); - }), - SizedBox(height: 12.h), - ], - ) - : SizedBox(height: 0.h); - }, - ), - SizedBox(height: 16.h), + // Consumer( + // builder: (context, emergencyServicesVM, child) { + // return emergencyServicesVM.patientHasAdvanceERBalance + // ? Column( + // children: [ + // SizedBox(height: 16.h), + // Container( + // decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + // color: AppColors.whiteColor, + // borderRadius: 20.r, + // hasShadow: false, + // side: BorderSide(color: AppColors.primaryRedColor, width: 3.h), + // ), + // width: double.infinity, + // child: Padding( + // padding: EdgeInsets.all(16.h), + // child: Column( + // crossAxisAlignment: CrossAxisAlignment.start, + // children: [ + // // Row( + // // mainAxisAlignment: MainAxisAlignment.spaceBetween, + // // children: [ + // // AppCustomChipWidget( + // // labelText: LocaleKeys.erOnlineCheckInRequest.tr(context: context), + // // backgroundColor: AppColors.primaryRedColor.withValues(alpha: 0.10), + // // textColor: AppColors.primaryRedColor, + // // ), + // // Utils.buildSvgWithAssets(icon: AppAssets.appointment_checkin_icon, width: 24.h, height: 24.h, iconColor: AppColors.primaryRedColor), + // // ], + // // ), + // SizedBox(height: 8.h), + // Row( + // mainAxisAlignment: MainAxisAlignment.spaceBetween, + // children: [ + // LocaleKeys.youHaveEROnlineCheckInRequest.tr(context: context).toText12(isBold: true), + // Transform.flip( + // flipX: getIt.get().isArabic(), + // child: Utils.buildSvgWithAssets( + // icon: AppAssets.forward_arrow_icon_small, + // iconColor: AppColors.blackColor, + // width: 20.h, + // height: 15.h, + // fit: BoxFit.contain, + // ), + // ), + // ], + // ), + // ], + // ), + // ), + // ).paddingSymmetrical(24.h, 0.h).onPress(() { + // Navigator.of(context).push(CustomPageRoute(page: ErOnlineCheckinHome())); + // // context.read().navigateToEROnlineCheckIn(); + // }), + // SizedBox(height: 12.h), + // ], + // ) + // : SizedBox(height: 0.h); + // }, + // ), + SizedBox(height: 16.h), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -575,66 +578,61 @@ class _LandingPageState extends State { Navigator.of(context).push(CustomPageRoute(page: MedicalFilePage())); }), SizedBox(height: 16.h), - Container( - // height: 121.h, - decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r), - child: Column( - children: [ - SizedBox( - height: 92.h + 32.h - 4.h, - child: RawScrollbar( - controller: _horizontalScrollController, - thumbVisibility: true, - radius: Radius.circular(10.0), - thumbColor: AppColors.primaryRedColor, - trackVisibility: true, - trackColor: Color(0xffD9D9D9), - trackBorderColor: Colors.transparent, - trackRadius: Radius.circular(10.0), - padding: EdgeInsets.only(top: 92.h + 32.h, left: MediaQuery - .sizeOf(context) - .width / 2 - 35, right: MediaQuery - .sizeOf(context) - .width / 2 - 35), - child: ListView.separated( - scrollDirection: Axis.horizontal, - itemCount: LandingPageData.getLoggedInServiceCardsList.length, - shrinkWrap: true, - controller: _horizontalScrollController, - padding: EdgeInsets.only(left: 16.h, right: 16.h, top: 16.h, bottom: 12.h), - itemBuilder: (context, index) { - return AnimationConfiguration.staggeredList( - position: index, - duration: const Duration(milliseconds: 1000), - child: SlideAnimation( - horizontalOffset: 100.0, - child: FadeInAnimation( - child: SmallServiceCard( - icon: LandingPageData.getLoggedInServiceCardsList[index].icon, - title: LandingPageData.getLoggedInServiceCardsList[index].title, - subtitle: LandingPageData.getLoggedInServiceCardsList[index].subtitle, - iconColor: LandingPageData.getLoggedInServiceCardsList[index].iconColor!, - textColor: LandingPageData.getLoggedInServiceCardsList[index].textColor, - backgroundColor: LandingPageData.getLoggedInServiceCardsList[index].backgroundColor, - isBold: LandingPageData.getLoggedInServiceCardsList[index].isBold, - serviceName: LandingPageData.getLoggedInServiceCardsList[index].serviceName, + Container( + // height: 121.h, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r), + child: Column( + children: [ + SizedBox( + height: 92.h + 32.h - 4.h, + child: RawScrollbar( + controller: _horizontalScrollController, + thumbVisibility: true, + radius: Radius.circular(10.0), + thumbColor: AppColors.primaryRedColor, + trackVisibility: true, + trackColor: Color(0xffD9D9D9), + trackBorderColor: Colors.transparent, + trackRadius: Radius.circular(10.0), + padding: EdgeInsets.only(top: 92.h + 32.h, left: MediaQuery.sizeOf(context).width / 2.5 - 10, right: MediaQuery.sizeOf(context).width / 2.5 - 10), + child: ListView.separated( + scrollDirection: Axis.horizontal, + itemCount: LandingPageData.getLoggedInServiceCardsList.length, + shrinkWrap: true, + controller: _horizontalScrollController, + padding: EdgeInsets.only(left: 0.h, right: 0.h, top: 16.h, bottom: 12.h), + itemBuilder: (context, index) { + return AnimationConfiguration.staggeredList( + position: index, + duration: const Duration(milliseconds: 1000), + child: SlideAnimation( + horizontalOffset: 100.0, + child: FadeInAnimation( + child: SmallServiceCard( + icon: LandingPageData.getLoggedInServiceCardsList[index].icon, + title: LandingPageData.getLoggedInServiceCardsList[index].title, + subtitle: LandingPageData.getLoggedInServiceCardsList[index].subtitle, + iconColor: LandingPageData.getLoggedInServiceCardsList[index].iconColor!, + textColor: LandingPageData.getLoggedInServiceCardsList[index].textColor, + backgroundColor: LandingPageData.getLoggedInServiceCardsList[index].backgroundColor, + isBold: LandingPageData.getLoggedInServiceCardsList[index].isBold, + serviceName: LandingPageData.getLoggedInServiceCardsList[index].serviceName, + ), + ), + ), + ); + }, + separatorBuilder: (BuildContext cxt, int index) => 10.width, + ).paddingSymmetrical(16.h, 0.h), ), ), - ), - ); - }, - separatorBuilder: (BuildContext cxt, int index) => 10.width, - ), - ), - ), - SizedBox(height: 16.h), - ], - ), - ).paddingSymmetrical(24.h, 0.h), - ], - ) - : Container( - // height: 141.h, + SizedBox(height: 16.h), + ], + ), + ).paddingSymmetrical(24.h, 0.h), + ], + ) + : Container( decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r), child: Column( children: [ @@ -649,14 +647,13 @@ class _LandingPageState extends State { trackColor: Color(0xffD9D9D9), trackBorderColor: Colors.transparent, trackRadius: Radius.circular(10.0), - padding: EdgeInsets.only(top: 92.h + 32.h, left: MediaQuery.sizeOf(context).width / 2 - 35, right: MediaQuery.sizeOf(context).width / 2 - 35), + padding: EdgeInsets.only(top: 92.h + 32.h, left: MediaQuery.sizeOf(context).width / 2.5 - 10, right: MediaQuery.sizeOf(context).width / 2.5 - 10), child: ListView.separated( scrollDirection: Axis.horizontal, itemCount: LandingPageData.getNotLoggedInServiceCardsList.length, shrinkWrap: true, controller: _horizontalScrollController, - padding: EdgeInsets.only(left: 16.h, right: 16.h, top: 16.h, bottom: 12.h), - // padding: EdgeInsets.zero, + padding: EdgeInsets.only(left: 0.h, right: 0.h, top: 16.h, bottom: 12.h), itemBuilder: (context, index) { return AnimationConfiguration.staggeredList( position: index, @@ -679,7 +676,7 @@ class _LandingPageState extends State { ); }, separatorBuilder: (BuildContext cxt, int index) => 0.width, - ), + ).paddingSymmetrical(16.h, 0.h), ), ), SizedBox(height: 16.h), @@ -733,7 +730,7 @@ class _LandingPageState extends State { ], ), ), - (!insuranceVM.isInsuranceLoading && insuranceVM.isInsuranceExpired && insuranceVM.isInsuranceExpiryBannerShown) + (appState.isAuthenticated && !insuranceVM.isInsuranceLoading && insuranceVM.isInsuranceExpired && insuranceVM.isInsuranceExpiryBannerShown) ? Container( height: MediaQuery.paddingOf(context).top + 50.h, decoration: ShapeDecoration( diff --git a/lib/presentation/home/service_info_page.dart b/lib/presentation/home/service_info_page.dart new file mode 100644 index 00000000..5b6b0f34 --- /dev/null +++ b/lib/presentation/home/service_info_page.dart @@ -0,0 +1,91 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/authentication/authentication_view_model.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; +import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; +import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; + +class ServiceInfoPage extends StatelessWidget { + final String serviceName; + final String serviceHeader; + final String serviceDescription; + final String serviceImage; + + const ServiceInfoPage({required this.serviceName, required this.serviceHeader, required this.serviceDescription, required this.serviceImage, super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.bgScaffoldColor, + body: Column( + children: [ + Expanded( + child: CollapsingListView( + title: "", + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(24.r), + child: Transform.flip( + flipX: getIt.get().isArabic(), + child: Image.asset( + serviceImage, + fit: BoxFit.fitHeight, + // height: 480.h, + // width: 520.w, + ), + ), + ).paddingSymmetrical(24.h, 0.h), + SizedBox(height: 32.h), + AppCustomChipWidget( + richText: serviceName.toText14(color: AppColors.infoColor, weight: FontWeight.w500), + backgroundColor: AppColors.infoColor.withAlpha(50), + textColor: AppColors.infoColor, + ).paddingSymmetrical(24.h, 0.h), + SizedBox(height: 24.h), + serviceHeader.toText28(isBold: true, height: 1.4).paddingSymmetrical(24.h, 0.h), + SizedBox(height: 24.h), + serviceDescription.toText14(weight: FontWeight.w500, color: AppColors.greyTextColor).paddingSymmetrical(24.h, 0.h), + SizedBox(height: 24.h), + ], + ), + ), + ), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: true, + ), + child: CustomButton( + text: LocaleKeys.login.tr(context: context), + onPressed: () { + getIt().onLoginPressed(); + }, + backgroundColor: AppColors.primaryRedColor, + borderColor: AppColors.primaryRedColor, + textColor: AppColors.whiteColor, + fontSize: 16.f, + fontWeight: FontWeight.w500, + borderRadius: 12.r, + padding: EdgeInsets.symmetric(horizontal: 10.w), + height: 56.h, + icon: AppAssets.login1, + iconColor: AppColors.whiteColor, + iconSize: 18.h, + ).paddingSymmetrical(24.h, 24.h), + ) + ], + ), + ); + } +} diff --git a/lib/presentation/home/widgets/habib_wallet_card.dart b/lib/presentation/home/widgets/habib_wallet_card.dart index 0ac32443..a8ba04dd 100644 --- a/lib/presentation/home/widgets/habib_wallet_card.dart +++ b/lib/presentation/home/widgets/habib_wallet_card.dart @@ -87,7 +87,8 @@ class HabibWalletCard extends StatelessWidget { fit: BoxFit.contain, ), SizedBox(width: 8.h), - habibWalletVM.habibWalletAmount + NumberFormat.decimalPattern() + .format(habibWalletVM.habibWalletAmount) .toString() .toText32(isBold: true, isEnglishOnly: true) .toShimmer2(isShow: habibWalletVM.isWalletAmountLoading, radius: 12.h, width: 80.h, height: 40.h), @@ -115,7 +116,7 @@ class HabibWalletCard extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ SizedBox( - width: 150.h, + width: 200.h, child: Utils.getPaymentMethods(), ), CustomButton( diff --git a/lib/presentation/home/widgets/large_service_card.dart b/lib/presentation/home/widgets/large_service_card.dart index b89e58c5..5428e21d 100644 --- a/lib/presentation/home/widgets/large_service_card.dart +++ b/lib/presentation/home/widgets/large_service_card.dart @@ -1,6 +1,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/api_consts.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; @@ -10,6 +11,7 @@ import 'package:hmg_patient_app_new/features/book_appointments/book_appointments import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/book_appointment_page.dart'; import 'package:hmg_patient_app_new/presentation/home/data/service_card_data.dart'; +import 'package:hmg_patient_app_new/presentation/home/service_info_page.dart'; import 'package:hmg_patient_app_new/presentation/home_health_care/hhc_procedures_page.dart'; import 'package:hmg_patient_app_new/services/navigation_service.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; @@ -135,11 +137,23 @@ class LargeServiceCard extends StatelessWidget { } case "home_health_care": { - Navigator.of(getIt().navigatorKey.currentContext!).push( - CustomPageRoute( - page: HhcProceduresPage(), - ), - ); + if (getIt.get().isAuthenticated) { + Navigator.of(getIt().navigatorKey.currentContext!).push( + CustomPageRoute( + page: HhcProceduresPage(), + ), + ); + } else { + Navigator.of(getIt().navigatorKey.currentContext!).push( + CustomPageRoute( + page: ServiceInfoPage( + serviceName: LocaleKeys.homeHealthCare.tr(), + serviceHeader: LocaleKeys.homeHealthCareServiceHeader.tr(), + serviceDescription: LocaleKeys.homeHealthCareServiceDescription.tr(), + serviceImage: AppAssets.homeHealthCareService), + ), + ); + } } case "pharmacy": { @@ -270,11 +284,23 @@ class FadedLargeServiceCard extends StatelessWidget { } case "home_health_care": { - Navigator.of(getIt().navigatorKey.currentContext!).push( - CustomPageRoute( - page: HhcProceduresPage(), - ), - ); + if (getIt.get().isAuthenticated) { + Navigator.of(getIt().navigatorKey.currentContext!).push( + CustomPageRoute( + page: HhcProceduresPage(), + ), + ); + } else { + Navigator.of(getIt().navigatorKey.currentContext!).push( + CustomPageRoute( + page: ServiceInfoPage( + serviceName: LocaleKeys.homeHealthCare.tr(), + serviceHeader: LocaleKeys.homeHealthCareServiceHeader.tr(), + serviceDescription: LocaleKeys.homeHealthCareServiceDescription.tr(), + serviceImage: AppAssets.homeHealthCareService), + ), + ); + } } case "pharmacy": { diff --git a/lib/presentation/home/widgets/welcome_widget.dart b/lib/presentation/home/widgets/welcome_widget.dart index 9b613e64..b2d4e3a1 100644 --- a/lib/presentation/home/widgets/welcome_widget.dart +++ b/lib/presentation/home/widgets/welcome_widget.dart @@ -1,12 +1,14 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/insurance/insurance_view_model.dart'; +import 'package:hmg_patient_app_new/features/profile_settings/profile_settings_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/insurance/insurance_home_page.dart'; import 'package:hmg_patient_app_new/presentation/profile_settings/profile_settings.dart'; @@ -40,7 +42,13 @@ class WelcomeWidget extends StatelessWidget { spacing: 8.h, children: [ Icon(Icons.menu, color: AppColors.textColor).onPress(() { - Navigator.of(context).push(springPageRoute(ProfileSettings())); + getIt.get().getProfileSettings(); + Navigator.of(context).push( + CustomPageRoute( + direction: getIt.get().isArabic() ? AxisDirection.right : AxisDirection.left, + page: ProfileSettings(), + ), + ); }), Image.asset(imageUrl, width: 40, height: 40), Column( @@ -56,7 +64,17 @@ class WelcomeWidget extends StatelessWidget { children: [ Flexible(child: name.toText16(weight: FontWeight.w500, textOverflow: TextOverflow.ellipsis, maxlines: 1, height: 1, isEnglishOnly: true)), // Icon(Icons.keyboard_arrow_down, size: 20, color: AppColors.greyTextColor), - Utils.buildSvgWithAssets(icon: AppAssets.arrowRight, height: 22.h, width: 22.w) + Transform.flip( + flipX: getIt.get().isArabic(), + child: Utils.buildSvgWithAssets( + icon: AppAssets.arrowRight, + iconColor: AppColors.blackColor, + width: 22.w, + height: 22.h, + fit: BoxFit.contain, + ) + ), + // Utils.buildSvgWithAssets(icon: AppAssets.arrowRight, height: 22.h, width: 22.w) ], ), ], diff --git a/lib/presentation/insurance/insurance_approval_details_page.dart b/lib/presentation/insurance/insurance_approval_details_page.dart index 7a69e088..e163b649 100644 --- a/lib/presentation/insurance/insurance_approval_details_page.dart +++ b/lib/presentation/insurance/insurance_approval_details_page.dart @@ -16,6 +16,8 @@ import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; import 'package:provider/provider.dart'; +import 'dart:ui' as ui; + class InsuranceApprovalDetailsPage extends StatelessWidget { InsuranceApprovalDetailsPage({super.key, required this.insuranceApprovalResponseModel}); @@ -36,7 +38,7 @@ class InsuranceApprovalDetailsPage extends StatelessWidget { decoration: RoundedRectangleBorder().toSmoothCornerDecoration( color: AppColors.whiteColor, borderRadius: 24.h, - hasShadow: true, + hasShadow: false, ), child: Padding( padding: EdgeInsets.all(14.h), @@ -84,21 +86,42 @@ class InsuranceApprovalDetailsPage extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ (insuranceApprovalResponseModel.doctorName!).toText16(isBold: true), + SizedBox(height: 8.h), Wrap( direction: Axis.horizontal, spacing: 3.h, runSpacing: 4.h, children: [ AppCustomChipWidget(labelText: insuranceApprovalResponseModel.clinicName!), - AppCustomChipWidget(labelText: "${LocaleKeys.approvalNo.tr(context: context)} ${insuranceApprovalResponseModel.approvalNo}"), - AppCustomChipWidget(labelText: "${LocaleKeys.unusedCount.tr(context: context)} ${insuranceApprovalResponseModel.unUsedCount}"), + AppCustomChipWidget(labelText: "${LocaleKeys.approvalNo.tr(context: context)} ${insuranceApprovalResponseModel.approvalNo}", isEnglishOnly: true), + AppCustomChipWidget(labelText: "${LocaleKeys.unusedCount.tr(context: context)} ${insuranceApprovalResponseModel.unUsedCount}", isEnglishOnly: true), AppCustomChipWidget(labelText: "${LocaleKeys.companyName.tr(context: context)} ${insuranceApprovalResponseModel.companyName}"), AppCustomChipWidget( - labelText: - "${LocaleKeys.receiptOn.tr(context: context)} ${DateUtil.formatDateToDate(DateUtil.convertStringToDate(insuranceApprovalResponseModel.receiptOn), false)}"), + richText: Row( + mainAxisSize: MainAxisSize.min, + children: [ + "${LocaleKeys.receiptOn.tr(context: context)} ".toText10(), + Directionality( + textDirection: ui.TextDirection.ltr, + child: DateUtil.formatDateToDate(DateUtil.convertStringToDate(insuranceApprovalResponseModel.receiptOn), false).toText10(isEnglishOnly: true)), + ], + ), + isEnglishOnly: true, + ), + AppCustomChipWidget( - labelText: - "${LocaleKeys.expiryOn.tr(context: context)} ${DateUtil.formatDateToDate(DateUtil.convertStringToDate(insuranceApprovalResponseModel.expiryDate), false)}"), + richText: Row( + mainAxisSize: MainAxisSize.min, + children: [ + "${LocaleKeys.expiryOn.tr(context: context)} ".toText10(), + Directionality( + textDirection: ui.TextDirection.ltr, + child: DateUtil.formatDateToDate(DateUtil.convertStringToDate(insuranceApprovalResponseModel.expiryDate), false).toText10(isEnglishOnly: true)), + ], + ), + isEnglishOnly: true, + ), + ], ), ], @@ -115,7 +138,7 @@ class InsuranceApprovalDetailsPage extends StatelessWidget { decoration: RoundedRectangleBorder().toSmoothCornerDecoration( color: AppColors.whiteColor, borderRadius: 24.h, - hasShadow: true, + hasShadow: false, ), child: Padding( padding: EdgeInsets.all(16.h), @@ -138,7 +161,6 @@ class InsuranceApprovalDetailsPage extends StatelessWidget { child: AnimatedContainer( duration: Duration(milliseconds: 300), curve: Curves.easeInOut, - decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.h, hasShadow: true), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ diff --git a/lib/presentation/insurance/insurance_home_page.dart b/lib/presentation/insurance/insurance_home_page.dart index 35e3acb8..1ac176e1 100644 --- a/lib/presentation/insurance/insurance_home_page.dart +++ b/lib/presentation/insurance/insurance_home_page.dart @@ -74,6 +74,7 @@ class _InsuranceHomePageState extends State { return Column( children: [ PatientInsuranceCard( + isShowButtons: index == 0, insuranceCardDetailsModel: insuranceVM.patientInsuranceList[index], isInsuranceExpired: DateTime.now().isAfter(DateUtil.convertStringToDate(insuranceVM.patientInsuranceList.first.cardValidTo))), SizedBox( diff --git a/lib/presentation/insurance/widgets/insurance_approval_card.dart b/lib/presentation/insurance/widgets/insurance_approval_card.dart index 8f5f7cfb..faedb58d 100644 --- a/lib/presentation/insurance/widgets/insurance_approval_card.dart +++ b/lib/presentation/insurance/widgets/insurance_approval_card.dart @@ -15,6 +15,8 @@ import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; +import 'dart:ui' as ui; + class InsuranceApprovalCard extends StatelessWidget { InsuranceApprovalCard({super.key, required this.insuranceApprovalResponseModel, required this.isLoading, required this.appState}); @@ -88,6 +90,7 @@ class InsuranceApprovalCard extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ (isLoading ? "John Smith" : insuranceApprovalResponseModel.doctorName!).toText16(isBold: true).toShimmer2(isShow: isLoading), + SizedBox(height: 8.h), Wrap( direction: Axis.horizontal, spacing: 3.h, @@ -98,14 +101,17 @@ class InsuranceApprovalCard extends StatelessWidget { // textColor: insuranceApprovalResponseModel.status == 9 ? AppColors.successColor : AppColors.primaryRedColor, // ).toShimmer2(isShow: isLoading), AppCustomChipWidget(labelText: isLoading ? "Cardiology" : insuranceApprovalResponseModel.clinicName!).toShimmer2(isShow: isLoading), - AppCustomChipWidget( - icon: AppAssets.doctor_calendar_icon, - labelText: isLoading ? "Cardiology" : DateUtil.formatDateToDate(DateUtil.convertStringToDate(insuranceApprovalResponseModel.submitOn), false)) - .toShimmer2(isShow: isLoading), + Directionality( + textDirection: ui.TextDirection.ltr, + child: AppCustomChipWidget( + icon: AppAssets.doctor_calendar_icon, + labelText: isLoading ? "Cardiology" : DateUtil.formatDateToDate(DateUtil.convertStringToDate(insuranceApprovalResponseModel.submitOn), false), isEnglishOnly: true,) + .toShimmer2(isShow: isLoading), + ), isLoading ? SizedBox.shrink() : AppCustomChipWidget( - labelText: isLoading ? LocaleKeys.approvalNo.tr(context: context) : "${LocaleKeys.approvalNo.tr(context: context)} ${insuranceApprovalResponseModel.approvalNo}") + labelText: isLoading ? LocaleKeys.approvalNo.tr(context: context) : "${LocaleKeys.approvalNo.tr(context: context)} ${insuranceApprovalResponseModel.approvalNo}", isEnglishOnly: true,) .toShimmer2(isShow: isLoading), ], ), diff --git a/lib/presentation/insurance/widgets/insurance_update_details_card.dart b/lib/presentation/insurance/widgets/insurance_update_details_card.dart index 770f4da2..51b9e05d 100644 --- a/lib/presentation/insurance/widgets/insurance_update_details_card.dart +++ b/lib/presentation/insurance/widgets/insurance_update_details_card.dart @@ -12,6 +12,7 @@ import 'package:hmg_patient_app_new/features/insurance/insurance_view_model.dart import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/lab/lab_result_item_view.dart'; +import 'package:hmg_patient_app_new/services/dialog_service.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; @@ -58,7 +59,7 @@ class PatientInsuranceCardUpdateCard extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ insuranceViewModel.patientInsuranceUpdateResponseModel!.memberName!.toText16(weight: FontWeight.w600), - "Policy: ${insuranceViewModel.patientInsuranceUpdateResponseModel!.policyNumber}".toText12(isBold: true, color: AppColors.lightGrayColor), + LocaleKeys.policyNumberInsurancePage.tr(namedArgs: {'number': insuranceViewModel.patientInsuranceUpdateResponseModel!.policyNumber.toString()}).toText12(isBold: true, color: AppColors.lightGrayColor, isEnglishOnly: true), SizedBox(height: 8.h), Row( children: [ @@ -86,9 +87,10 @@ class PatientInsuranceCardUpdateCard extends StatelessWidget { icon: AppAssets.doctor_calendar_icon, labelText: "${LocaleKeys.expiryOn.tr(context: context)} ${DateUtil.formatDateToDate(DateTime.parse(insuranceViewModel.patientInsuranceUpdateResponseModel!.effectiveTo!), false)}", + isEnglishOnly: true, ), AppCustomChipWidget( - labelText: "Member ID: ${insuranceViewModel.patientInsuranceUpdateResponseModel!.memberID!}", + labelText: "Member ID: ${insuranceViewModel.patientInsuranceUpdateResponseModel!.memberID!}", isEnglishOnly: true, ), ], ), @@ -121,16 +123,27 @@ class PatientInsuranceCardUpdateCard extends StatelessWidget { insuranceCardImage: "", onSuccess: (val) { LoaderBottomSheet.hideLoader(); - showCommonBottomSheetWithoutHeight( - title: LocaleKeys.success.tr(context: context), - context, - child: Utils.getSuccessWidget(loadingText: LocaleKeys.insuranceRequestSubmittedSuccessfully.tr(context: context)), - callBackFunc: () { - Navigator.pop(context); - }, - isFullScreen: false, - isCloseButtonVisible: false, - ); + getIt.get().showCommonBottomSheetWithoutH( + message: LocaleKeys.insuranceRequestSubmittedSuccessfully.tr(context: context), + label: LocaleKeys.notice.tr(), + onOkPressed: () { + Navigator.pop(context); + }, + okLabel: "confirm", + cancelLabel: LocaleKeys.acknowledged.tr(context: context), + isConfirmButton: true, + ); + // showCommonBottomSheetWithoutHeight( + // title: LocaleKeys.success.tr(context: context), + // context, + // child: Utils.getSuccessWidget(loadingText: LocaleKeys.insuranceRequestSubmittedSuccessfully.tr(context: context)), + // callBackFunc: () { + // Navigator.pop(context); + // }, + // isFullScreen: false, + // isCloseButtonVisible: false, + // isAutoDismiss: true + // ); // Future.delayed(Duration(milliseconds: 2000)).then((value) async { // Navigator.pop(context); // }); diff --git a/lib/presentation/insurance/widgets/patient_insurance_card.dart b/lib/presentation/insurance/widgets/patient_insurance_card.dart index 1607a7c6..16cbac20 100644 --- a/lib/presentation/insurance/widgets/patient_insurance_card.dart +++ b/lib/presentation/insurance/widgets/patient_insurance_card.dart @@ -20,10 +20,11 @@ import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher.dart'; class PatientInsuranceCard extends StatelessWidget { - PatientInsuranceCard({super.key, required this.insuranceCardDetailsModel, required this.isInsuranceExpired}); + PatientInsuranceCard({super.key, required this.insuranceCardDetailsModel, required this.isInsuranceExpired, this.isShowButtons = true}); PatientInsuranceDetailsResponseModel insuranceCardDetailsModel; bool isInsuranceExpired = false; + bool isShowButtons = true; late InsuranceViewModel insuranceViewModel; late AppState appState; @@ -53,14 +54,15 @@ class PatientInsuranceCard extends StatelessWidget { SizedBox( width: MediaQuery.of(context).size.width * 0.4, child: "${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}".toText18(isBold: true, textOverflow: TextOverflow.clip, isEnglishOnly: true)), - Row( - children: [ - "${LocaleKeys.policyNumber.tr(context: context)}${insuranceCardDetailsModel.insurancePolicyNo}".toText12(isBold: true, color: AppColors.lightGrayColor), - ], - ), + LocaleKeys.policyNumberInsurancePage.tr(namedArgs: {'number': insuranceCardDetailsModel.insurancePolicyNo.toString()}).toText12(isBold: true, color: AppColors.lightGrayColor, isEnglishOnly: true), + // Row( + // children: [ + // "${LocaleKeys.policyNumber.tr(context: context)}${insuranceCardDetailsModel.insurancePolicyNo}".toText12(isBold: true, color: AppColors.lightGrayColor), + // ], + // ), ], ), - AppCustomChipWidget( + isShowButtons ? AppCustomChipWidget( icon: isCurrentPatientInsuranceExpired(insuranceCardDetailsModel.cardValidTo!) ? AppAssets.cancel_circle_icon : insuranceViewModel.isInsuranceActive @@ -120,7 +122,7 @@ class PatientInsuranceCard extends StatelessWidget { ? AppColors.successColor.withOpacity(0.1) : AppColors.warningColorYellow.withOpacity(0.1), labelPadding: EdgeInsetsDirectional.only(start: -4.w, end: insuranceViewModel.isInsuranceActive ? 6.w : 0.w), - ).toShimmer2(isShow: insuranceViewModel.isInsuranceLoading), + ).toShimmer2(isShow: insuranceViewModel.isInsuranceLoading) : SizedBox.shrink(), // AppCustomChipWidget( // icon: isInsuranceExpired ? AppAssets.cancel_circle_icon : AppAssets.insurance_active_icon, // labelText: isInsuranceExpired ? LocaleKeys.insuranceExpired.tr(context: context) : LocaleKeys.insuranceActive.tr(context: context), @@ -136,7 +138,7 @@ class PatientInsuranceCard extends StatelessWidget { insuranceCardDetailsModel.groupName!.toText12(isBold: true), Row( children: [ - insuranceCardDetailsModel.companyName!.toText12(isBold: true), + (insuranceCardDetailsModel.companyName!.length > 40 ? "${insuranceCardDetailsModel.companyName!.substring(0, 40)}..." : insuranceCardDetailsModel.companyName!).toText12(isBold: true), SizedBox( width: 6.h, ), @@ -146,7 +148,7 @@ class PatientInsuranceCard extends StatelessWidget { color: AppColors.infoColor, borderRadius: 50.r, ), - child: (insuranceCardDetailsModel.subCategoryDesc!.length > 5 ? insuranceCardDetailsModel.subCategoryDesc!.substring(0, 12) : insuranceCardDetailsModel.subCategoryDesc!) + child: ((insuranceCardDetailsModel.subCategoryDesc ?? "").length > 10 ? (insuranceCardDetailsModel.subCategoryDesc ?? "").substring(0, 10) : (insuranceCardDetailsModel.subCategoryDesc ?? "")) .toText8(isBold: true, color: AppColors.whiteColor), ), ], @@ -168,7 +170,7 @@ class PatientInsuranceCard extends StatelessWidget { ), SizedBox(height: 10.h), isInsuranceExpired - ? CustomButton( + ? isShowButtons ? CustomButton( icon: AppAssets.update_insurance_card_icon, iconColor: AppColors.warningColorYellow, iconSize: 15.h, @@ -194,7 +196,7 @@ class PatientInsuranceCard extends StatelessWidget { borderRadius: 12, padding: EdgeInsets.fromLTRB(10, 0, 10, 0), height: 40.h, - ) + ) : SizedBox.shrink() : Container(), ], ), diff --git a/lib/presentation/lab/alphabeticScroll.dart b/lib/presentation/lab/alphabeticScroll.dart index 95d9da45..933f399c 100644 --- a/lib/presentation/lab/alphabeticScroll.dart +++ b/lib/presentation/lab/alphabeticScroll.dart @@ -45,41 +45,55 @@ class _AlphabetScrollPageState extends State { void initState() { super.initState(); scheduleMicrotask((){ - for(var char in widget.alpahbetsAvailable){ - data[char] = widget.details.where((element)=>element.description?.toLowerCase().startsWith(char.toLowerCase()) == true).toList(); - - } - setState((){}); - + _rebuildData(); }); - itemPositionsListener.itemPositions.addListener((){ + itemPositionsListener.itemPositions.addListener(_onPositionChanged); + } + + @override + void didUpdateWidget(covariant AlphabeticScroll oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.details != widget.details || oldWidget.alpahbetsAvailable != widget.alpahbetsAvailable) { + _rebuildData(); + } + } - final positions = itemPositionsListener.itemPositions.value; + void _rebuildData() { + data.clear(); + for (var char in widget.alpahbetsAvailable) { + data[char] = widget.details + .where((element) => element.description?.toLowerCase().startsWith(char.toLowerCase()) == true) + .toList(); + } + if (_activeIndex >= widget.alpahbetsAvailable.length) { + _activeIndex = 0; + } + setState(() {}); + } - if (positions.isEmpty) return; + void _onPositionChanged() { + final positions = itemPositionsListener.itemPositions.value; - // Get FIRST visible item (top-most) - final firstVisible = positions - .where((p) => p.itemTrailingEdge > 0) // visible - .reduce((min, p) => - p.itemLeadingEdge < min.itemLeadingEdge ? p : min); + if (positions.isEmpty) return; - if(_activeIndex == firstVisible.index) return ; - setState(() { - _activeIndex = firstVisible.index; - }); + // Get FIRST visible item (top-most) + final firstVisible = positions + .where((p) => p.itemTrailingEdge > 0) // visible + .reduce((min, p) => + p.itemLeadingEdge < min.itemLeadingEdge ? p : min); - print("Active index = $_activeIndex"); + if(_activeIndex == firstVisible.index) return ; + setState(() { + _activeIndex = firstVisible.index; }); + + print("Active index = $_activeIndex"); } @override void dispose() { - itemPositionsListener.itemPositions.removeListener((){ - - }); + itemPositionsListener.itemPositions.removeListener(_onPositionChanged); super.dispose(); - } void _scrollToLetter(String letter) async { @@ -145,7 +159,6 @@ class _AlphabetScrollPageState extends State { crossAxisAlignment: CrossAxisAlignment.center, children:List.generate(widget.alpahbetsAvailable.length, (i) { final isActive = (i == _activeIndex); - return GestureDetector( onTap: () { setState(() => _activeIndex = i); @@ -161,7 +174,7 @@ class _AlphabetScrollPageState extends State { child: Opacity( opacity: isActive ? 1.0 : 0.5, child: widget.alpahbetsAvailable[i].toUpperCase().toText14( - color: !isActive ? AppColors.greyTextColor : AppColors.primaryRedColor + color: !isActive ? AppColors.greyTextColor : AppColors.primaryRedColor, isEnglishOnly: true ), ), ); diff --git a/lib/presentation/lab/lab_order_by_test.dart b/lib/presentation/lab/lab_order_by_test.dart index ac6f0f3b..5cb94320 100644 --- a/lib/presentation/lab/lab_order_by_test.dart +++ b/lib/presentation/lab/lab_order_by_test.dart @@ -11,6 +11,7 @@ import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; +import 'dart:ui' as ui; class LabOrderByTest extends StatelessWidget { final VoidCallback onTap; @@ -36,7 +37,7 @@ class LabOrderByTest extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - '${tests!.description}'.toText16(isBold: true), + '${tests!.description}'.toText16(isBold: true, isEnglishOnly: true), SizedBox(height: 4.h), (appState.isArabic() ? tests!.testDescriptionAr : tests!.testDescriptionEn)!.toText12(fontWeight: FontWeight.w500), SizedBox(height: 8.h), @@ -44,7 +45,13 @@ class LabOrderByTest extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ AppCustomChipWidget( - richText: '${"${LocaleKeys.lastTested.tr(context: context)}:"} ${DateUtil.formatDateToDate(DateUtil.convertStringToDate(tests!.createdOn), false)}'.toText12(fontWeight: FontWeight.w500), + richText: Row( + children: [ + "${LocaleKeys.lastTested.tr(context: context)}: ".toText12(fontWeight: FontWeight.w500), + Directionality( + textDirection: ui.TextDirection.ltr,child: DateUtil.formatDateToDate(DateUtil.convertStringToDate(tests!.createdOn), false).toText12(fontWeight: FontWeight.w500, isEnglishOnly: true)) + ], + ), backgroundColor: AppColors.greyLightColor, textColor: AppColors.textColor, ), diff --git a/lib/presentation/lab/lab_orders_page.dart b/lib/presentation/lab/lab_orders_page.dart index 900477e4..b3ef6cde 100644 --- a/lib/presentation/lab/lab_orders_page.dart +++ b/lib/presentation/lab/lab_orders_page.dart @@ -1 +1 @@ -import 'dart:async'; import 'dart:convert'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/enums.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/lab/lab_view_model.dart'; import 'package:hmg_patient_app_new/features/lab/models/resp_models/patient_lab_orders_response_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/lab/lab_result_item_view.dart'; import 'package:hmg_patient_app_new/presentation/lab/lab_result_via_clinic/LabResultByClinic.dart'; import 'package:hmg_patient_app_new/presentation/lab/search_lab_report.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/utils/date_util.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_toolbar.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; import 'package:hmg_patient_app_new/widgets/chip/custom_chip_widget.dart'; import 'package:hmg_patient_app_new/widgets/custom_tab_bar.dart'; import 'package:hmg_patient_app_new/widgets/date_range_selector/viewmodel/date_range_view_model.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:provider/provider.dart'; import 'alphabeticScroll.dart'; import 'dart:ui' as ui; class LabOrdersPage extends StatefulWidget { const LabOrdersPage({super.key}); @override State createState() => _LabOrdersPageState(); } class _LabOrdersPageState extends State { late LabViewModel labProvider; late DateRangeSelectorRangeViewModel rangeViewModel; late AppState _appState; List?> labSuggestions = []; int? expandedIndex; String? selectedFilterText = ''; int activeIndex = 0; @override void initState() { scheduleMicrotask(() { labProvider.initLabProvider(); }); super.initState(); } @override Widget build(BuildContext context) { labProvider = Provider.of(context, listen: false); rangeViewModel = Provider.of(context); _appState = getIt(); return CollapsingListView( title: LocaleKeys.labResults.tr(context: context), search: () async { if (labProvider.isLabOrdersLoading) { return; } else { String? value = await Navigator.of(context).push( CustomPageRoute( page: SearchLabResultsContent(labSuggestionsList: labProvider.labSuggestions), fullScreenDialog: true, direction: AxisDirection.down, ), ); if (value != null) { selectedFilterText = value; labProvider.filterLabReports(value); } } }, child: Consumer( builder: (context, labViewModel, child) { return SingleChildScrollView( physics: NeverScrollableScrollPhysics(), padding: EdgeInsets.all(24.h), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: CustomTabBar( activeTextColor: Color(0xffED1C2B), activeBackgroundColor: Color(0xffED1C2B).withValues(alpha: .1), tabs: [ CustomTabBarModel(null, LocaleKeys.byVisit.tr()), CustomTabBarModel(null, LocaleKeys.byTest.tr()), // CustomTabBarModel(null, "Completed".needTranslation), ], onTabChange: (index) { activeIndex = index; setState(() {}); }, ), ), ], ), if (activeIndex == 0) Padding( padding: EdgeInsets.symmetric(vertical: 10.h), child: Row( children: [ // CustomButton( // text: LocaleKeys.byClinic.tr(context: context), // onPressed: () { // labViewModel.setIsSortByClinic(true); // }, // backgroundColor: labViewModel.isSortByClinic ? AppColors.bgRedLightColor : AppColors.whiteColor, // borderColor: labViewModel.isSortByClinic ? AppColors.primaryRedColor : AppColors.textColor.withValues(alpha: 0.2), // textColor: labViewModel.isSortByClinic ? AppColors.primaryRedColor : AppColors.blackColor, // fontSize: 12.f, // fontWeight: FontWeight.w500, // borderRadius: 10, // padding: EdgeInsets.fromLTRB(10, 0, 10, 0), // height: 40.h, // ), // SizedBox(width: 8.h), // CustomButton( // text: LocaleKeys.byHospital.tr(context: context), // onPressed: () { // labViewModel.setIsSortByClinic(false); // }, // backgroundColor: labViewModel.isSortByClinic ? AppColors.whiteColor : AppColors.bgRedLightColor, // borderColor: labViewModel.isSortByClinic ? AppColors.textColor.withValues(alpha: 0.2) : AppColors.primaryRedColor, // textColor: labViewModel.isSortByClinic ? AppColors.blackColor : AppColors.primaryRedColor, // fontSize: 12, // fontWeight: FontWeight.w500, // borderRadius: 10, // padding: EdgeInsets.fromLTRB(10, 0, 10, 0), // height: 40.h, // ), ], ), ), SizedBox(height: 8.h), selectedFilterText!.isNotEmpty ? Column( children: [ AppCustomChipWidget( labelText: selectedFilterText!, backgroundColor: AppColors.alertColor, textColor: AppColors.whiteColor, deleteIcon: AppAssets.close_bottom_sheet_icon, deleteIconColor: AppColors.whiteColor, deleteIconHasColor: true, onDeleteTap: () { selectedFilterText = ""; labProvider.filterLabReports(""); }, ), SizedBox(height: 8.h), ], ) : SizedBox(), activeIndex == 0 ? // By Visit - show grouped view when available labViewModel.isLabOrdersLoading ? ListView.builder( shrinkWrap: true, physics: AlwaysScrollableScrollPhysics(), padding: EdgeInsets.zero, itemCount: 5, itemBuilder: (context, index) => LabResultItemView( onTap: () {}, labOrder: null, index: index, isLoading: true, ), ) : (labViewModel.patientLabOrders.isNotEmpty ? ListView.builder( shrinkWrap: true, physics: NeverScrollableScrollPhysics(), padding: EdgeInsets.zero, itemCount: labViewModel.patientLabOrders.length, itemBuilder: (context, index) { final group = labViewModel.patientLabOrders[index]; final isExpanded = expandedIndex == index; return AnimationConfiguration.staggeredList( position: index, duration: const Duration(milliseconds: 500), child: SlideAnimation( verticalOffset: 100.0, child: FadeInAnimation( child: AnimatedContainer( duration: Duration(milliseconds: 300), curve: Curves.easeInOut, margin: EdgeInsets.symmetric(vertical: 8.h), decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 20.h, hasShadow: true), child: InkWell( onTap: () { setState(() { expandedIndex = isExpanded ? null : index; }); }, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: EdgeInsets.all(16.h), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Wrap( direction: Axis.horizontal, spacing: 4.h, runSpacing: 4.h, children: [ AppCustomChipWidget( labelText: "${group.testDetails!.length} ${LocaleKeys.tests.tr(context: context)}", backgroundColor: AppColors.successColor.withOpacity(0.1), textColor: AppColors.successColor, ), AppCustomChipWidget( labelText: "${_appState.isArabic() ? group.isInOutPatientDescriptionN : group.isInOutPatientDescription}", backgroundColor: AppColors.warningColorYellow.withOpacity(0.1), textColor: AppColors.warningColorYellow, ) ], ), Icon(isExpanded ? Icons.expand_less : Icons.expand_more), ], ), SizedBox(height: 8.h), Row( mainAxisSize: MainAxisSize.min, children: [ Image.network( group.doctorImageURL ?? "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png", width: 24.w, height: 24.h, fit: BoxFit.cover, ).circle(100), SizedBox(width: 8.h), Expanded(child: (group.doctorName ?? group.doctorNameEnglish ?? "").toString().toText14(weight: FontWeight.w500)), ], ), SizedBox(height: 8.h), Wrap( direction: Axis.horizontal, spacing: 4.h, runSpacing: 4.h, children: [ Directionality( textDirection: ui.TextDirection.ltr, child: AppCustomChipWidget( labelText: DateUtil.formatDateToDate(DateUtil.convertStringToDate(group.orderDate ?? ""), false), isEnglishOnly: true, )), AppCustomChipWidget( labelText: (group.projectName ?? ""), ), AppCustomChipWidget( labelText: (group.clinicDescription ?? ""), ), ], ), ], ), ), AnimatedSwitcher( duration: Duration(milliseconds: 500), switchInCurve: Curves.easeIn, switchOutCurve: Curves.easeOut, transitionBuilder: (Widget child, Animation animation) { return FadeTransition( opacity: animation, child: SizeTransition( sizeFactor: animation, axisAlignment: 0.0, child: child, ), ); }, child: isExpanded ? Container( key: ValueKey(index), padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 0.h), child: Column( children: [ ListView.separated( shrinkWrap: true, physics: NeverScrollableScrollPhysics(), padding: EdgeInsets.zero, itemBuilder: (cxt, index) { PatientLabOrdersResponseModel order = group; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ "• ${order.testDetails![index].description!}".toText14(weight: FontWeight.w500), SizedBox(height: 4.h), order.testDetails![index].testDescriptionEn!.toText12(fontWeight: FontWeight.w500, color: AppColors.textColorLight), // Row( // mainAxisSize: MainAxisSize.min, // children: [ // Image.network( // order.doctorImageURL ?? "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png", // width: 24.w, // height: 24.h, // fit: BoxFit.cover, // ).circle(100), // SizedBox(width: 8.h), // Expanded(child: (order.doctorName ?? order.doctorNameEnglish ?? "").toString().toText14(weight: FontWeight.w500)), // ], // ), // SizedBox(height: 8.h), // Wrap( // direction: Axis.horizontal, // spacing: 4.h, // runSpacing: 4.h, // children: [ // AppCustomChipWidget( // labelText: ("${LocaleKeys.orderNo.tr(context: context)}: ${order.orderNo!}"), isEnglishOnly: true, // ), // Directionality( // textDirection: ui.TextDirection.ltr, // child: AppCustomChipWidget( // labelText: DateUtil.formatDateToDate(DateUtil.convertStringToDate(order.orderDate ?? ""), false), // isEnglishOnly: true, // )), // AppCustomChipWidget( // labelText: labViewModel.isSortByClinic ? (order.projectName ?? "") : (order.clinicDescription ?? ""), // ), // ], // ), // // Row( // // children: [ // // CustomButton( // // text: ("Order No: ".needTranslation + order.orderNo!), // // onPressed: () {}, // // backgroundColor: AppColors.greyColor, // // borderColor: AppColors.greyColor, // // textColor: AppColors.blackColor, // // fontSize: 10, // // fontWeight: FontWeight.w500, // // borderRadius: 8, // // padding: EdgeInsets.fromLTRB(10, 0, 10, 0), // // height: 24.h, // // ), // // SizedBox(width: 8.h), // // CustomButton( // // text: DateUtil.formatDateToDate(DateUtil.convertStringToDate(order.orderDate ?? ""), false), // // onPressed: () {}, // // backgroundColor: AppColors.greyColor, // // borderColor: AppColors.greyColor, // // textColor: AppColors.blackColor, // // fontSize: 10, // // fontWeight: FontWeight.w500, // // borderRadius: 8, // // padding: EdgeInsets.fromLTRB(10, 0, 10, 0), // // height: 24.h, // // ), // // ], // // ), // // SizedBox(height: 8.h), // // Row( // // children: [ // // CustomButton( // // text: model.isSortByClinic ? (order.clinicDescription ?? "") : (order.projectName ?? ""), // // onPressed: () {}, // // backgroundColor: AppColors.greyColor, // // borderColor: AppColors.greyColor, // // textColor: AppColors.blackColor, // // fontSize: 10, // // fontWeight: FontWeight.w500, // // borderRadius: 8, // // padding: EdgeInsets.fromLTRB(10, 0, 10, 0), // // height: 24.h, // // ), // // ], // // ), // SizedBox(height: 12.h), // Row( // children: [ // Expanded(flex: 2, child: SizedBox()), // // Expanded( // // flex: 1, // // child: Container( // // height: 40.h, // // width: 40.w, // // decoration: RoundedRectangleBorder().toSmoothCornerDecoration( // // color: AppColors.textColor, // // borderRadius: 12, // // ), // // child: Padding( // // padding: EdgeInsets.all(12.h), // // child: Transform.flip( // // flipX: _appState.isArabic(), // // child: Utils.buildSvgWithAssets( // // icon: AppAssets.forward_arrow_icon_small, // // iconColor: AppColors.whiteColor, // // fit: BoxFit.contain, // // ), // // ), // // ), // // ).onPress(() { // // model.currentlySelectedPatientOrder = order; // // labProvider.getPatientLabResultByHospital(order); // // labProvider.getPatientSpecialResult(order); // // Navigator.of(context).push( // // CustomPageRoute(page: LabResultByClinic(labOrder: order)), // // ); // // }), // // ) // // Expanded( // flex: 2, // child: CustomButton( // icon: AppAssets.view_report_icon, // iconColor: AppColors.primaryRedColor, // iconSize: 16.h, // text: LocaleKeys.viewResults.tr(context: context), // onPressed: () { // labViewModel.currentlySelectedPatientOrder = order; // labProvider.getPatientLabResultByHospital(order); // labProvider.getPatientSpecialResult(order); // Navigator.of(context).push( // CustomPageRoute(page: LabResultByClinic(labOrder: order)), // ); // }, // backgroundColor: AppColors.secondaryLightRedColor, // borderColor: AppColors.secondaryLightRedColor, // textColor: AppColors.primaryRedColor, // fontSize: 14, // fontWeight: FontWeight.w500, // borderRadius: 12, // padding: EdgeInsets.fromLTRB(10, 0, 10, 0), // height: 40.h, // ), // ) // ], // ), // SizedBox(height: 12.h), // Divider(color: AppColors.borderOnlyColor.withValues(alpha: 0.05), height: 1.h), // SizedBox(height: 12.h), ], ).paddingOnly(top: 16, bottom: 16); }, separatorBuilder: (cxt, index) => Divider(color: AppColors.borderOnlyColor.withValues(alpha: 0.05), height: 1.h), itemCount: group.testDetails!.length > 3 ? 3 : group.testDetails!.length), SizedBox(height: 16.h), CustomButton( text: "${LocaleKeys.viewResults.tr()} (${group.testDetails!.length})", onPressed: () { labProvider.currentlySelectedPatientOrder = group; labProvider.getPatientLabResultByHospital(group); labProvider.getPatientSpecialResult(group); Navigator.of(context).push( CustomPageRoute( page: LabResultByClinic(labOrder: group), ), ); }, backgroundColor: AppColors.infoColor.withAlpha(20), borderColor: AppColors.infoColor.withAlpha(0), textColor: AppColors.infoColor, fontSize: (isFoldable || isTablet) ? 12.f : 14.f, fontWeight: FontWeight.w500, borderRadius: 12.r, padding: EdgeInsets.fromLTRB(10.w, 0, 10.w, 0), height: 40.h, iconSize: 14.h, icon: AppAssets.view_report_icon, iconColor: AppColors.infoColor, ), SizedBox(height: 16.h), ], ), ) : SizedBox.shrink(), ), ], ), ), ), ), )); }, ) : Utils.getNoDataWidget(context, noDataText: LocaleKeys.noLabResults.tr(context: context))) : // By Test or other tabs keep existing behavior (labViewModel.isLabOrdersLoading) ? Column( children: List.generate( 5, (index) => LabResultItemView( onTap: () {}, labOrder: null, index: index, isLoading: true, )), ) : AlphabeticScroll( alpahbetsAvailable: labViewModel.indexedCharacterForUniqueTest, details: labViewModel.uniqueTestsList, labViewModel: labViewModel, rangeViewModel: rangeViewModel, appState: _appState, ) ], )); }, ), ); } Color getLabOrderStatusColor(num status) { switch (status) { case 44: return AppColors.warningColorYellow; case 45: return AppColors.warningColorYellow; case 16: return AppColors.successColor; case 17: return AppColors.successColor; default: return AppColors.greyColor; } } String getLabOrderStatusText(num status) { switch (status) { case 44: return LocaleKeys.resultsPending.tr(context: context); case 45: return LocaleKeys.resultsPending.tr(context: context); case 16: return LocaleKeys.resultsAvailable.tr(context: context); case 17: return LocaleKeys.resultsAvailable.tr(context: context); default: return ""; } } getLabSuggestions(LabViewModel model) { if (model.patientLabOrders.isEmpty) { return []; } return model.patientLabOrders.map((m) => m.testDetails).toList(); } } \ No newline at end of file +import 'dart:async'; import 'dart:convert'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/enums.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/lab/lab_view_model.dart'; import 'package:hmg_patient_app_new/features/lab/models/resp_models/patient_lab_orders_response_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/lab/lab_result_item_view.dart'; import 'package:hmg_patient_app_new/presentation/lab/lab_result_via_clinic/LabResultByClinic.dart'; import 'package:hmg_patient_app_new/presentation/lab/search_lab_report.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/utils/date_util.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_toolbar.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; import 'package:hmg_patient_app_new/widgets/chip/custom_chip_widget.dart'; import 'package:hmg_patient_app_new/widgets/custom_tab_bar.dart'; import 'package:hmg_patient_app_new/widgets/date_range_selector/viewmodel/date_range_view_model.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:provider/provider.dart'; import 'alphabeticScroll.dart'; import 'dart:ui' as ui; class LabOrdersPage extends StatefulWidget { const LabOrdersPage({super.key}); @override State createState() => _LabOrdersPageState(); } class _LabOrdersPageState extends State { late LabViewModel labProvider; late DateRangeSelectorRangeViewModel rangeViewModel; late AppState _appState; List?> labSuggestions = []; int? expandedIndex; String? selectedFilterText = ''; int activeIndex = 0; @override void initState() { scheduleMicrotask(() { labProvider.initLabProvider(); }); super.initState(); } @override Widget build(BuildContext context) { labProvider = Provider.of(context, listen: false); rangeViewModel = Provider.of(context); _appState = getIt(); return CollapsingListView( title: LocaleKeys.labResults.tr(context: context), search: () async { if (labProvider.isLabOrdersLoading) { return; } else { String? value = await Navigator.of(context).push( CustomPageRoute( page: SearchLabResultsContent(labSuggestionsList: labProvider.labSuggestions), fullScreenDialog: true, direction: AxisDirection.down, ), ); if (value != null) { selectedFilterText = value; labProvider.filterLabReports(value); } } }, child: Consumer( builder: (context, labViewModel, child) { return SingleChildScrollView( physics: NeverScrollableScrollPhysics(), padding: EdgeInsets.all(24.h), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: CustomTabBar( activeTextColor: Color(0xffED1C2B), activeBackgroundColor: Color(0xffED1C2B).withValues(alpha: .1), tabs: [ CustomTabBarModel(null, LocaleKeys.byVisit.tr()), CustomTabBarModel(null, LocaleKeys.byTest.tr()), // CustomTabBarModel(null, "Completed".needTranslation), ], onTabChange: (index) { activeIndex = index; setState(() {}); }, ), ), ], ), if (activeIndex == 0) Padding( padding: EdgeInsets.symmetric(vertical: 10.h), child: Row( children: [ // CustomButton( // text: LocaleKeys.byClinic.tr(context: context), // onPressed: () { // labViewModel.setIsSortByClinic(true); // }, // backgroundColor: labViewModel.isSortByClinic ? AppColors.bgRedLightColor : AppColors.whiteColor, // borderColor: labViewModel.isSortByClinic ? AppColors.primaryRedColor : AppColors.textColor.withValues(alpha: 0.2), // textColor: labViewModel.isSortByClinic ? AppColors.primaryRedColor : AppColors.blackColor, // fontSize: 12.f, // fontWeight: FontWeight.w500, // borderRadius: 10, // padding: EdgeInsets.fromLTRB(10, 0, 10, 0), // height: 40.h, // ), // SizedBox(width: 8.h), // CustomButton( // text: LocaleKeys.byHospital.tr(context: context), // onPressed: () { // labViewModel.setIsSortByClinic(false); // }, // backgroundColor: labViewModel.isSortByClinic ? AppColors.whiteColor : AppColors.bgRedLightColor, // borderColor: labViewModel.isSortByClinic ? AppColors.textColor.withValues(alpha: 0.2) : AppColors.primaryRedColor, // textColor: labViewModel.isSortByClinic ? AppColors.blackColor : AppColors.primaryRedColor, // fontSize: 12, // fontWeight: FontWeight.w500, // borderRadius: 10, // padding: EdgeInsets.fromLTRB(10, 0, 10, 0), // height: 40.h, // ), ], ), ), SizedBox(height: 8.h), selectedFilterText!.isNotEmpty ? Column( children: [ AppCustomChipWidget( labelText: selectedFilterText!, backgroundColor: AppColors.alertColor, textColor: AppColors.whiteColor, deleteIcon: AppAssets.close_bottom_sheet_icon, deleteIconColor: AppColors.whiteColor, deleteIconHasColor: true, isEnglishOnly: true, onDeleteTap: () { selectedFilterText = ""; labProvider.filterLabReports(""); }, ), SizedBox(height: 8.h), ], ) : SizedBox(), activeIndex == 0 ? // By Visit - show grouped view when available labViewModel.isLabOrdersLoading ? ListView.builder( shrinkWrap: true, physics: AlwaysScrollableScrollPhysics(), padding: EdgeInsets.zero, itemCount: 5, itemBuilder: (context, index) => LabResultItemView( onTap: () {}, labOrder: null, index: index, isLoading: true, ), ) : (labViewModel.patientLabOrders.isNotEmpty ? ListView.builder( shrinkWrap: true, physics: NeverScrollableScrollPhysics(), padding: EdgeInsets.zero, itemCount: labViewModel.filteredLabOrders.length, itemBuilder: (context, index) { final group = labViewModel.filteredLabOrders[index]; final isExpanded = expandedIndex == index; return AnimationConfiguration.staggeredList( position: index, duration: const Duration(milliseconds: 500), child: SlideAnimation( verticalOffset: 100.0, child: FadeInAnimation( child: AnimatedContainer( duration: Duration(milliseconds: 300), curve: Curves.easeInOut, margin: EdgeInsets.symmetric(vertical: 8.h), decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 20.h, hasShadow: true), child: InkWell( onTap: () { setState(() { expandedIndex = isExpanded ? null : index; }); }, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: EdgeInsets.all(16.h), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Wrap( direction: Axis.horizontal, spacing: 4.h, runSpacing: 4.h, children: [ AppCustomChipWidget( labelText: "${group.testDetails!.length} ${LocaleKeys.tests.tr(context: context)}", backgroundColor: AppColors.successColor.withOpacity(0.1), textColor: AppColors.successColor, ), AppCustomChipWidget( labelText: "${_appState.isArabic() ? group.isInOutPatientDescriptionN : group.isInOutPatientDescription}", backgroundColor: AppColors.warningColorYellow.withOpacity(0.1), textColor: AppColors.warningColorYellow, ) ], ), Icon(isExpanded ? Icons.expand_less : Icons.expand_more), ], ), SizedBox(height: 8.h), Row( mainAxisSize: MainAxisSize.min, children: [ Image.network( group.doctorImageURL ?? "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png", width: 24.w, height: 24.h, fit: BoxFit.cover, ).circle(100), SizedBox(width: 8.h), Expanded(child: (group.doctorName ?? group.doctorNameEnglish ?? "").toString().toText14(weight: FontWeight.w500)), ], ), SizedBox(height: 8.h), Wrap( direction: Axis.horizontal, spacing: 4.h, runSpacing: 4.h, children: [ Directionality( textDirection: ui.TextDirection.ltr, child: AppCustomChipWidget( labelText: DateUtil.formatDateToDate(DateUtil.convertStringToDate(group.orderDate ?? ""), false), isEnglishOnly: true, )), AppCustomChipWidget( labelText: (group.projectName ?? ""), ), AppCustomChipWidget( labelText: (group.clinicDescription ?? ""), ), ], ), ], ), ), AnimatedSwitcher( duration: Duration(milliseconds: 500), switchInCurve: Curves.easeIn, switchOutCurve: Curves.easeOut, transitionBuilder: (Widget child, Animation animation) { return FadeTransition( opacity: animation, child: SizeTransition( sizeFactor: animation, axisAlignment: 0.0, child: child, ), ); }, child: isExpanded ? Container( key: ValueKey(index), padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 0.h), child: Column( children: [ ListView.separated( shrinkWrap: true, physics: NeverScrollableScrollPhysics(), padding: EdgeInsets.zero, itemBuilder: (cxt, index) { PatientLabOrdersResponseModel order = group; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ "• ${order.testDetails![index].description!}".toText14(weight: FontWeight.w500), SizedBox(height: 4.h), order.testDetails![index].testDescriptionEn!.toText12(fontWeight: FontWeight.w500, color: AppColors.textColorLight), // Row( // mainAxisSize: MainAxisSize.min, // children: [ // Image.network( // order.doctorImageURL ?? "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png", // width: 24.w, // height: 24.h, // fit: BoxFit.cover, // ).circle(100), // SizedBox(width: 8.h), // Expanded(child: (order.doctorName ?? order.doctorNameEnglish ?? "").toString().toText14(weight: FontWeight.w500)), // ], // ), // SizedBox(height: 8.h), // Wrap( // direction: Axis.horizontal, // spacing: 4.h, // runSpacing: 4.h, // children: [ // AppCustomChipWidget( // labelText: ("${LocaleKeys.orderNo.tr(context: context)}: ${order.orderNo!}"), isEnglishOnly: true, // ), // Directionality( // textDirection: ui.TextDirection.ltr, // child: AppCustomChipWidget( // labelText: DateUtil.formatDateToDate(DateUtil.convertStringToDate(order.orderDate ?? ""), false), // isEnglishOnly: true, // )), // AppCustomChipWidget( // labelText: labViewModel.isSortByClinic ? (order.projectName ?? "") : (order.clinicDescription ?? ""), // ), // ], // ), // // Row( // // children: [ // // CustomButton( // // text: ("Order No: ".needTranslation + order.orderNo!), // // onPressed: () {}, // // backgroundColor: AppColors.greyColor, // // borderColor: AppColors.greyColor, // // textColor: AppColors.blackColor, // // fontSize: 10, // // fontWeight: FontWeight.w500, // // borderRadius: 8, // // padding: EdgeInsets.fromLTRB(10, 0, 10, 0), // // height: 24.h, // // ), // // SizedBox(width: 8.h), // // CustomButton( // // text: DateUtil.formatDateToDate(DateUtil.convertStringToDate(order.orderDate ?? ""), false), // // onPressed: () {}, // // backgroundColor: AppColors.greyColor, // // borderColor: AppColors.greyColor, // // textColor: AppColors.blackColor, // // fontSize: 10, // // fontWeight: FontWeight.w500, // // borderRadius: 8, // // padding: EdgeInsets.fromLTRB(10, 0, 10, 0), // // height: 24.h, // // ), // // ], // // ), // // SizedBox(height: 8.h), // // Row( // // children: [ // // CustomButton( // // text: model.isSortByClinic ? (order.clinicDescription ?? "") : (order.projectName ?? ""), // // onPressed: () {}, // // backgroundColor: AppColors.greyColor, // // borderColor: AppColors.greyColor, // // textColor: AppColors.blackColor, // // fontSize: 10, // // fontWeight: FontWeight.w500, // // borderRadius: 8, // // padding: EdgeInsets.fromLTRB(10, 0, 10, 0), // // height: 24.h, // // ), // // ], // // ), // SizedBox(height: 12.h), // Row( // children: [ // Expanded(flex: 2, child: SizedBox()), // // Expanded( // // flex: 1, // // child: Container( // // height: 40.h, // // width: 40.w, // // decoration: RoundedRectangleBorder().toSmoothCornerDecoration( // // color: AppColors.textColor, // // borderRadius: 12, // // ), // // child: Padding( // // padding: EdgeInsets.all(12.h), // // child: Transform.flip( // // flipX: _appState.isArabic(), // // child: Utils.buildSvgWithAssets( // // icon: AppAssets.forward_arrow_icon_small, // // iconColor: AppColors.whiteColor, // // fit: BoxFit.contain, // // ), // // ), // // ), // // ).onPress(() { // // model.currentlySelectedPatientOrder = order; // // labProvider.getPatientLabResultByHospital(order); // // labProvider.getPatientSpecialResult(order); // // Navigator.of(context).push( // // CustomPageRoute(page: LabResultByClinic(labOrder: order)), // // ); // // }), // // ) // // Expanded( // flex: 2, // child: CustomButton( // icon: AppAssets.view_report_icon, // iconColor: AppColors.primaryRedColor, // iconSize: 16.h, // text: LocaleKeys.viewResults.tr(context: context), // onPressed: () { // labViewModel.currentlySelectedPatientOrder = order; // labProvider.getPatientLabResultByHospital(order); // labProvider.getPatientSpecialResult(order); // Navigator.of(context).push( // CustomPageRoute(page: LabResultByClinic(labOrder: order)), // ); // }, // backgroundColor: AppColors.secondaryLightRedColor, // borderColor: AppColors.secondaryLightRedColor, // textColor: AppColors.primaryRedColor, // fontSize: 14, // fontWeight: FontWeight.w500, // borderRadius: 12, // padding: EdgeInsets.fromLTRB(10, 0, 10, 0), // height: 40.h, // ), // ) // ], // ), // SizedBox(height: 12.h), // Divider(color: AppColors.borderOnlyColor.withValues(alpha: 0.05), height: 1.h), // SizedBox(height: 12.h), ], ).paddingOnly(top: 16, bottom: 16); }, separatorBuilder: (cxt, index) => Divider(color: AppColors.borderOnlyColor.withValues(alpha: 0.05), height: 1.h), itemCount: group.testDetails!.length > 3 ? 3 : group.testDetails!.length), SizedBox(height: 16.h), CustomButton( text: "${LocaleKeys.viewResults.tr()} (${group.testDetails!.length})", onPressed: () { labProvider.currentlySelectedPatientOrder = group; labProvider.getPatientLabResultByHospital(group); labProvider.getPatientSpecialResult(group); Navigator.of(context).push( CustomPageRoute( page: LabResultByClinic(labOrder: group), ), ); }, backgroundColor: AppColors.infoColor.withAlpha(20), borderColor: AppColors.infoColor.withAlpha(0), textColor: AppColors.infoColor, fontSize: (isFoldable || isTablet) ? 12.f : 14.f, fontWeight: FontWeight.w500, borderRadius: 12.r, padding: EdgeInsets.fromLTRB(10.w, 0, 10.w, 0), height: 40.h, iconSize: 14.h, icon: AppAssets.view_report_icon, iconColor: AppColors.infoColor, ), SizedBox(height: 16.h), ], ), ) : SizedBox.shrink(), ), ], ), ), ), ), )); }, ) : Utils.getNoDataWidget(context, noDataText: LocaleKeys.noLabResults.tr(context: context))) : // By Test or other tabs keep existing behavior (labViewModel.isLabOrdersLoading) ? Column( children: List.generate( 5, (index) => LabResultItemView( onTap: () {}, labOrder: null, index: index, isLoading: true, )), ) : AlphabeticScroll( alpahbetsAvailable: labViewModel.filteredIndexedCharacterForUniqueTest, details: labViewModel.filteredUniqueTestsList, labViewModel: labViewModel, rangeViewModel: rangeViewModel, appState: _appState, ) ], )); }, ), ); } Color getLabOrderStatusColor(num status) { switch (status) { case 44: return AppColors.warningColorYellow; case 45: return AppColors.warningColorYellow; case 16: return AppColors.successColor; case 17: return AppColors.successColor; default: return AppColors.greyColor; } } String getLabOrderStatusText(num status) { switch (status) { case 44: return LocaleKeys.resultsPending.tr(context: context); case 45: return LocaleKeys.resultsPending.tr(context: context); case 16: return LocaleKeys.resultsAvailable.tr(context: context); case 17: return LocaleKeys.resultsAvailable.tr(context: context); default: return ""; } } getLabSuggestions(LabViewModel model) { if (model.patientLabOrders.isEmpty) { return []; } return model.patientLabOrders.map((m) => m.testDetails).toList(); } } \ No newline at end of file diff --git a/lib/presentation/lab/lab_result_via_clinic/LabResultList.dart b/lib/presentation/lab/lab_result_via_clinic/LabResultList.dart index 07b9fb65..6f68a6c1 100644 --- a/lib/presentation/lab/lab_result_via_clinic/LabResultList.dart +++ b/lib/presentation/lab/lab_result_via_clinic/LabResultList.dart @@ -27,7 +27,7 @@ class LabResultList extends StatelessWidget { shrinkWrap: true,itemCount: list.length,itemBuilder: (____, index) { var labItem = list[index]; return LabOrderResultItem(onTap: () { - model.getPatientLabResult(model.currentlySelectedPatientOrder!, labItem.description ?? "", labItem.testShortDescription!, labItem.uOM ?? ""); + model.getPatientLabResult(model.currentlySelectedPatientOrder!, labItem.description ?? "", labItem.testShortDescription ?? "", labItem.uOM ?? ""); }, tests: labItem, index: index, diff --git a/lib/presentation/lab/lab_result_via_clinic/lab_order_result_item.dart b/lib/presentation/lab/lab_result_via_clinic/lab_order_result_item.dart index 1a97d48d..ac2537e1 100644 --- a/lib/presentation/lab/lab_result_via_clinic/lab_order_result_item.dart +++ b/lib/presentation/lab/lab_result_via_clinic/lab_order_result_item.dart @@ -44,7 +44,7 @@ class LabOrderResultItem extends StatelessWidget { ), // (tests!.packageShortDescription ?? "").toText12(fontWeight: FontWeight.w500, color: AppColors.textColorLight), - ((tests!.testShortDescription != null && tests!.testShortDescription!.isNotEmpty) ? tests!.testShortDescription : tests!.packageShortDescription)! + (((tests!.testShortDescription != null && tests!.testShortDescription!.isNotEmpty) ? tests!.testShortDescription : tests!.packageShortDescription) ?? "") .toText12(fontWeight: FontWeight.w500, color: AppColors.textColorLight), SizedBox(height: 12.h), Directionality( diff --git a/lib/presentation/lab/lab_results/lab_result_details.dart b/lib/presentation/lab/lab_results/lab_result_details.dart index 69254f69..aeef4240 100644 --- a/lib/presentation/lab/lab_results/lab_result_details.dart +++ b/lib/presentation/lab/lab_results/lab_result_details.dart @@ -23,6 +23,7 @@ import 'package:hmg_patient_app_new/widgets/graph/custom_graph.dart'; import 'package:provider/provider.dart' show Consumer, Provider, ReadContext, Selector; import '../../../widgets/common_bottom_sheet.dart' show showCommonBottomSheetWithoutHeight; +import 'dart:ui' as ui; class LabResultDetails extends StatelessWidget { final LabResult recentLabResult; @@ -63,6 +64,30 @@ class LabResultDetails extends StatelessWidget { LabNameAndStatus(context), getLabDescription(context), LabGraph(context), + Container( + padding: EdgeInsets.symmetric(horizontal: 24.h, vertical: 16.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.h, + hasShadow: true, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + LocaleKeys.history.tr(context: context).toText16(isBold: true, color: AppColors.textColor), + (labVM.isLabResultsHistoryShowMore ? "Show Less" : "Show More").toText14(weight: FontWeight.w500, color: AppColors.primaryRedColor, isUnderLine: true).onPress(() { + labVM.setIsLabResultsHistoryShowMore(); + }), + ], + ), + SizedBox(height: 24.h,), + labHistoryList(labVM), + ], + ), + ), Selector( selector: (_, model) => model.labOrderResponseByAi, builder: (_, aiData, __) { @@ -170,7 +195,7 @@ class LabResultDetails extends StatelessWidget { ], ), SizedBox(height: 4.h), - ("${LocaleKeys.resultOf.tr(context: context)} ${recentLabResult.verifiedOn ?? ""}").toText11(weight: FontWeight.w500, color: AppColors.greyTextColor), + ("${LocaleKeys.resultOf.tr(context: context)} ${recentLabResult.verifiedOn ?? ""}").toText11(weight: FontWeight.w500, color: AppColors.greyTextColor, isEnglishOnly: true), ], ), Row( @@ -232,24 +257,25 @@ class LabResultDetails extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - labmodel.isGraphVisible ? LocaleKeys.historyFlowchart.tr(context: context) : LocaleKeys.history.tr(context: context), - style: TextStyle( - fontSize: 16.f, - fontFamily: 'Poppins', - fontWeight: FontWeight.w600, - color: AppColors.textColor, - ), - ), + (labmodel.isGraphVisible ? LocaleKeys.historyFlowchart.tr(context: context) : LocaleKeys.history.tr(context: context)).toText16(isBold: true, color: AppColors.textColor), + // Text( + // labmodel.isGraphVisible ? LocaleKeys.historyFlowchart.tr(context: context) : LocaleKeys.history.tr(context: context), + // style: TextStyle( + // fontSize: 16.f, + // fontFamily: 'Poppins', + // fontWeight: FontWeight.w600, + // color: AppColors.textColor, + // ), + // ), Row( spacing: 16.h, children: [ //todo handle when the graph icon is being displayed - Utils.buildSvgWithAssets(icon: labmodel.isGraphVisible ? AppAssets.ic_list : AppAssets.ic_graph, width: 24.h, height: 24.h).onPress(() { - if (labmodel.shouldShowGraph) { - labmodel.alterGraphVisibility(); - } - }), + // Utils.buildSvgWithAssets(icon: labmodel.isGraphVisible ? AppAssets.ic_list : AppAssets.ic_graph, width: 24.h, height: 24.h).onPress(() { + // if (labmodel.shouldShowGraph) { + // labmodel.alterGraphVisibility(); + // } + // }), Utils.buildSvgWithAssets(icon: AppAssets.ic_date_filter, width: 24, height: 24).onPress(() { showCommonBottomSheetWithoutHeight( title: LocaleKeys.setTheDateRange.tr(context: context), @@ -311,6 +337,7 @@ class LabResultDetails extends StatelessWidget { minY: labmodel.minY, maxX: labmodel.filteredGraphValues.length.toDouble() - .75, horizontalInterval: .1, + isRTL: getIt.get().isArabic(), getDrawingHorizontalLine: (value) { value = double.parse(value.toStringAsFixed(1)); if (value == labmodel.highRefrenceValue || value == labmodel.lowRefenceValue) { @@ -328,20 +355,20 @@ class LabResultDetails extends StatelessWidget { }, leftLabelFormatter: (value) { value = double.parse(value.toStringAsFixed(1)); - // return leftLabels(value.toStringAsFixed(2)); - if (value == labmodel.highRefrenceValue) { - return leftLabels(LocaleKeys.high.tr()); - } - - if (value == labmodel.lowRefenceValue) { - return leftLabels(LocaleKeys.low.tr()); - } + return leftLabels(value.toStringAsFixed(2)); + // if (value == labmodel.highRefrenceValue) { + // return leftLabels(LocaleKeys.high.tr()); + // } + // + // if (value == labmodel.lowRefenceValue) { + // return leftLabels(LocaleKeys.low.tr()); + // } return SizedBox.shrink(); // } }, - graphColor: AppColors.blackColor, - graphShadowColor: Colors.transparent, + graphColor: AppColors.graphGridColor, + graphShadowColor: AppColors.graphGridColor.withOpacity(.5), graphGridColor: graphColor.withOpacity(.4), bottomLabelFormatter: (value, data) { if (data.isEmpty) return SizedBox.shrink(); @@ -373,7 +400,7 @@ class LabResultDetails extends StatelessWidget { ranges.add(HorizontalRangeAnnotation( y1: model.minY, y2: model.lowRefenceValue, - color: AppColors.highAndLow.withOpacity(0.05), + color: Colors.transparent, )); ranges.add(HorizontalRangeAnnotation( @@ -385,17 +412,18 @@ class LabResultDetails extends StatelessWidget { ranges.add(HorizontalRangeAnnotation( y1: model.highRefrenceValue, y2: model.maxY, - color: AppColors.criticalLowAndHigh.withOpacity(0.05), + color: Colors.transparent, )); return ranges; } Widget labHistoryList(LabViewModel labmodel) { return SizedBox( - height: labmodel.filteredGraphValues.length < 3 ? labmodel.filteredGraphValues.length * 64 : 180.h, + height: labmodel.filteredGraphValues.length < 3 ? labmodel.filteredGraphValues.length * 64 : (!labmodel.isLabResultsHistoryShowMore ? 180.h : 220.h), child: ListView.separated( padding: EdgeInsets.zero, - itemCount: labmodel.filteredGraphValues.length, + // itemCount: labmodel.isLabResultsHistoryShowMore ? (labmodel.filteredGraphValues.length < 3 ? labmodel.filteredGraphValues : 3) : labmodel.filteredGraphValues.length, + itemCount: !labmodel.isLabResultsHistoryShowMore ? (labmodel.filteredGraphValues.length < 3 ? labmodel.filteredGraphValues.length : 3) : labmodel.filteredGraphValues.length, itemBuilder: (context, index) { var data = labmodel.filteredGraphValues.reversed.toList()[index]; return LabHistoryItem( @@ -414,15 +442,17 @@ class LabResultDetails extends StatelessWidget { } double? getInterval(LabViewModel labmodel) { - return .1; - // var maxX = labmodel.maxY; - // if(maxX<1) return .5; - // if(maxX >1 && maxX < 5) return 1; - // if(maxX >5 && maxX < 10) return 5; - // if(maxX >10 && maxX < 50) return 10; - // if(maxX >50 && maxX < 100) return 20; - // if(maxX >100 && maxX < 200) return 30; - // return 50; + // return .1; + var maxX = labmodel.maxY; + if(maxX<1) return .3; + if(maxX >1 && maxX <= 5) return .7; + if(maxX >5 && maxX <= 10) return 2.5; + if(maxX >10 && maxX <= 50) return 5; + if(maxX >50 && maxX <= 100) return 10; + if(maxX >100 && maxX <= 200) return 30; + if(maxX >200 && maxX <= 300) return 50; + if(maxX >300 && maxX <= 400) return 100; + return 200 ; } Widget getLabDescription(BuildContext context) { diff --git a/lib/presentation/lab/search_lab_report.dart b/lib/presentation/lab/search_lab_report.dart index d366f38f..be57150f 100644 --- a/lib/presentation/lab/search_lab_report.dart +++ b/lib/presentation/lab/search_lab_report.dart @@ -78,8 +78,9 @@ class _SearchLabResultsContentState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ TextInputWidget( - labelText: "Search lab results", - hintText: "Type test name", + fontFamily: "Poppins", + labelText: LocaleKeys.searchLabResults.tr(context: context), + hintText: "", controller: searchEditingController, isEnable: true, prefix: null, @@ -93,7 +94,7 @@ class _SearchLabResultsContentState extends State { ), SizedBox(height: ResponsiveExtension(20).h), if (filteredSuggestions.isNotEmpty) ...[ - "Suggestions".toText16(isBold: true), + "Suggestions".toText16(isBold: true, isEnglishOnly: true), ], ], ), @@ -147,6 +148,7 @@ class SuggestionChip extends StatelessWidget { // color: isSelected ? AppColors.textColor : Colors.black87, color: AppColors.textColor, fontWeight: FontWeight.w500, + isEnglishOnly: true ), ), ); diff --git a/lib/presentation/medical_file/medical_file_page.dart b/lib/presentation/medical_file/medical_file_page.dart index cddc4757..a5a42e3b 100644 --- a/lib/presentation/medical_file/medical_file_page.dart +++ b/lib/presentation/medical_file/medical_file_page.dart @@ -199,11 +199,12 @@ class _MedicalFilePageState extends State { ], ), SizedBox(width: 4.h), - Utils.buildSvgWithAssets(icon: AppAssets.arrowRight, height: 22.h, width: 22.w) + Transform.flip(flipX: appState.isArabic(), child: Utils.buildSvgWithAssets(icon: AppAssets.arrowRight, height: 22.h, width: 22.w)) ], ).onPress(() { Navigator.of(context).push( CustomPageRoute( + direction: AxisDirection.down, page: FamilyMedicalScreen(), ), ); @@ -473,8 +474,7 @@ class _MedicalFilePageState extends State { ], ); }), - SizedBox(height: 16.h), - + SizedBox(height: 24.h), // TextInputWidget( // labelText: LocaleKeys.search.tr(context: context), // hintText: "Type any record".needTranslation, @@ -489,7 +489,7 @@ class _MedicalFilePageState extends State { // leadingIcon: AppAssets.search_icon, // hintColor: AppColors.textColor, // ).paddingSymmetrical(24.w, 0.0), - SizedBox(height: 16.h), + // SizedBox(height: 16.h), // Using CustomExpandableList CustomExpandableList( expansionMode: ExpansionMode.exactlyOne, @@ -547,7 +547,7 @@ class _MedicalFilePageState extends State { doctorImageURL: patientAppointmentHistoryResponseModel.doctorImageURL, doctorTitle: patientAppointmentHistoryResponseModel.doctorTitle, name: patientAppointmentHistoryResponseModel.doctorNameObj, - nationalityFlagURL: "https://hmgwebservices.com/Images/flag/SYR.png", + nationalityFlagURL: "", speciality: [], clinicName: patientAppointmentHistoryResponseModel.clinicName, projectName: patientAppointmentHistoryResponseModel.projectName, @@ -1538,6 +1538,7 @@ class _MedicalFilePageState extends State { value: vitalSign.bodyMassIndex?.toString() ?? '--', unit: '', status: vitalSign.bodyMassIndex != null ? _getBMIStatus(vitalSign.bodyMassIndex) : null, + // status: "Overweight", onTap: onTap, ), ), @@ -1566,7 +1567,7 @@ class _MedicalFilePageState extends State { label: LocaleKeys.weight.tr(context: context), value: vitalSign.weightKg?.toString() ?? '--', unit: 'kg', - status: vitalSign.weightKg != null ? "Normal" : null, + status: vitalSign.weightKg != null ? _getWeightStatus(vitalSign.bodyMassIndex) : null, onTap: onTap, ), ), @@ -1593,6 +1594,11 @@ class _MedicalFilePageState extends State { return VitalSignUiModel.bmiStatus(bmi); } + String? _getWeightStatus(dynamic bmi) { + if (bmi == null) return 'Normal'; + return VitalSignUiModel.bmiStatus(bmi); + } + String? _getBloodPressureStatus({dynamic systolic, dynamic diastolic}) { return VitalSignUiModel.bloodPressureStatus(systolic: systolic, diastolic: diastolic); } diff --git a/lib/presentation/medical_report/medical_reports_page.dart b/lib/presentation/medical_report/medical_reports_page.dart index fd3ba7f9..9dccdf79 100644 --- a/lib/presentation/medical_report/medical_reports_page.dart +++ b/lib/presentation/medical_report/medical_reports_page.dart @@ -419,6 +419,7 @@ class _MedicalReportsPageState extends State { callBackFunc: () {}, isFullScreen: false, isCloseButtonVisible: true, + isAutoDismiss: true ); } } diff --git a/lib/presentation/my_family/my_family.dart b/lib/presentation/my_family/my_family.dart index dca75747..66b31cfc 100644 --- a/lib/presentation/my_family/my_family.dart +++ b/lib/presentation/my_family/my_family.dart @@ -43,6 +43,7 @@ class _FamilyMedicalScreenState extends State { AppState appState = getIt.get(); return CollapsingListView( + isClose: true, title: LocaleKeys.familyTitle.tr(context: context), bottomChild: appState.getAuthenticatedUser()!.isParentUser! ? Container( diff --git a/lib/presentation/my_invoices/my_invoices_details_page.dart b/lib/presentation/my_invoices/my_invoices_details_page.dart index 79e1adc3..a605160d 100644 --- a/lib/presentation/my_invoices/my_invoices_details_page.dart +++ b/lib/presentation/my_invoices/my_invoices_details_page.dart @@ -6,6 +6,7 @@ import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/utils/date_util.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/int_extensions.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/my_invoices/models/get_invoice_details_response_model.dart'; @@ -19,6 +20,8 @@ import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; import 'package:provider/provider.dart'; +import 'dart:ui' as ui; + class MyInvoicesDetailsPage extends StatefulWidget { GetInvoiceDetailsResponseModel getInvoiceDetailsResponseModel; GetInvoicesListResponseModel getInvoicesListResponseModel; @@ -55,6 +58,7 @@ class _MyInvoicesDetailsPageState extends State { callBackFunc: () {}, isFullScreen: false, isCloseButtonVisible: true, + isAutoDismiss: true ); }, onError: (err) { @@ -145,7 +149,7 @@ class _MyInvoicesDetailsPageState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - (getIt().isArabic() ? widget.getInvoiceDetailsResponseModel.doctorNameN! : widget.getInvoiceDetailsResponseModel.doctorName!).toText16(isBold: true), + (getIt().isArabic() ? widget.getInvoiceDetailsResponseModel.doctorNameN ?? LocaleKeys.doctor.tr(context: context) : widget.getInvoiceDetailsResponseModel.doctorName!).toText16(isBold: true), SizedBox(height: 8.h), Wrap( direction: Axis.horizontal, @@ -153,23 +157,28 @@ class _MyInvoicesDetailsPageState extends State { runSpacing: 6.h, children: [ AppCustomChipWidget( - labelText: "${LocaleKeys.invoiceNo}: ${widget.getInvoiceDetailsResponseModel.invoiceNo!}", + labelText: "${LocaleKeys.invoiceNo.tr(context: context)}: ${widget.getInvoiceDetailsResponseModel.invoiceNo!}", labelPadding: EdgeInsetsDirectional.only(start: 6.w, end: 6.w), + isEnglishOnly: true, ), AppCustomChipWidget( - labelText: (widget.getInvoiceDetailsResponseModel.clinicDescription!.length > 15 + labelText: ((getIt().isArabic() ? widget.getInvoiceDetailsResponseModel.clinicDescriptionN : widget.getInvoiceDetailsResponseModel.clinicDescription ?? "")!.length > 15 ? '${widget.getInvoiceDetailsResponseModel.clinicDescription!.substring(0, 12)}...' - : widget.getInvoiceDetailsResponseModel.clinicDescription!), + : getIt().isArabic() ? widget.getInvoiceDetailsResponseModel.clinicDescriptionN : widget.getInvoiceDetailsResponseModel.clinicDescription ?? ""), labelPadding: EdgeInsetsDirectional.only(start: 4.w, end: 4.w), ), AppCustomChipWidget( labelText: widget.getInvoiceDetailsResponseModel.projectName!, labelPadding: EdgeInsetsDirectional.only(start: 6.w, end: 6.w), ), - AppCustomChipWidget( - labelPadding: EdgeInsetsDirectional.only(start: -4.w, end: 6.w), - icon: AppAssets.doctor_calendar_icon, - labelText: DateUtil.formatDateToDate(DateUtil.convertStringToDate(widget.getInvoiceDetailsResponseModel.appointmentDate), false), + Directionality( + textDirection: ui.TextDirection.ltr, + child: AppCustomChipWidget( + labelPadding: EdgeInsetsDirectional.only(start: -4.w, end: 6.w), + icon: AppAssets.doctor_calendar_icon, + labelText: DateUtil.formatDateToDate(DateUtil.convertStringToDate(widget.getInvoiceDetailsResponseModel.appointmentDate), false), + isEnglishOnly: true, + ), ), ], ), @@ -183,48 +192,6 @@ class _MyInvoicesDetailsPageState extends State { ), ), SizedBox(height: 16.h), - Row( - mainAxisSize: MainAxisSize.max, - children: [ - Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 20.h, - hasShadow: true, - ), - child: Padding( - padding: EdgeInsets.all(14.h), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - widget.getInvoiceDetailsResponseModel.listConsultation!.first.procedureName!.toText16(isBold: true), - SizedBox(height: 16.h), - Wrap( - direction: Axis.horizontal, - spacing: 6.w, - runSpacing: 6.h, - children: [ - AppCustomChipWidget( - labelText: "${LocaleKeys.quantity.tr()}: ${widget.getInvoiceDetailsResponseModel.listConsultation!.first.quantity!}", - labelPadding: EdgeInsetsDirectional.only(start: 6.w, end: 6.w), - ), - AppCustomChipWidget( - labelText: "${LocaleKeys.price.tr()}: ${widget.getInvoiceDetailsResponseModel.listConsultation!.first.price!} ${LocaleKeys.sar.tr()}", - labelPadding: EdgeInsetsDirectional.only(start: 6.w, end: 6.w), - ), - AppCustomChipWidget( - labelText: "${LocaleKeys.total.tr()}: ${widget.getInvoiceDetailsResponseModel.listConsultation!.first.total!} ${LocaleKeys.sar.tr()}", - labelPadding: EdgeInsetsDirectional.only(start: 6.w, end: 6.w), - ), - ], - ), - ], - ), - ), - ), - ], - ), - SizedBox(height: 16.h), Container( decoration: RoundedRectangleBorder().toSmoothCornerDecoration( color: AppColors.whiteColor, @@ -236,12 +203,12 @@ class _MyInvoicesDetailsPageState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - "Insurance Details".toText16(isBold: true), + LocaleKeys.insurance.tr(context: context).toText16(isBold: true), SizedBox(height: 16.h), - widget.getInvoiceDetailsResponseModel.groupName!.toText14(isBold: true), + (getIt().isArabic() ? widget.getInvoiceDetailsResponseModel.groupNameN : widget.getInvoiceDetailsResponseModel.groupName)!.toText14(isBold: true), Row( children: [ - Expanded(child: widget.getInvoiceDetailsResponseModel.companyName!.toText14(isBold: true)), + Expanded(child: (widget.getInvoiceDetailsResponseModel.companyName ?? "").toText14(isBold: true)), ], ), SizedBox(height: 12.h), @@ -250,6 +217,7 @@ class _MyInvoicesDetailsPageState extends State { AppCustomChipWidget( labelText: "Insurance ID: ${widget.getInvoiceDetailsResponseModel.insuranceID ?? "-"}", labelPadding: EdgeInsetsDirectional.only(start: 6.w, end: 6.w), + isEnglishOnly: true, ), ], ), @@ -257,6 +225,57 @@ class _MyInvoicesDetailsPageState extends State { ), ), ), + SizedBox(height: 16.h), + Row( + mainAxisSize: MainAxisSize.max, + children: [ + Expanded( + child: Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 20.h, + hasShadow: true, + ), + child: Padding( + padding: EdgeInsets.all(14.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header row + Row( + children: [ + Expanded(flex: 3, child: LocaleKeys.name.tr().toText12(color: AppColors.textColor, fontWeight: FontWeight.w600)), + Expanded(flex: 1, child: LocaleKeys.quantity.tr().toText12(color: AppColors.textColor, fontWeight: FontWeight.w600, isCenter: true)), + Expanded( + flex: 2, child: LocaleKeys.price.tr().toText12(color: AppColors.textColor, fontWeight: FontWeight.w600, isCenter: true)), + Expanded( + flex: 2, child: LocaleKeys.total.tr().toText12(color: AppColors.textColor, fontWeight: FontWeight.w600, isCenter: true)), + ], + ), + SizedBox(height: 16.h), + 2.divider, + SizedBox(height: 12.h), + // Data rows + ...widget.getInvoiceDetailsResponseModel.listConsultation!.map( + (consultation) => Padding( + padding: EdgeInsets.symmetric(vertical: 6.h), + child: Row( + children: [ + Expanded(flex: 3, child: (consultation.procedureName ?? '-').toText12(fontWeight: FontWeight.w500)), + Expanded(flex: 1, child: '${consultation.quantity ?? '-'}'.toText12(fontWeight: FontWeight.w500, isCenter: true, isEnglishOnly: true)), + Expanded(flex: 2, child: (NumberFormat.decimalPattern().format(consultation.price) ?? '-').toText12(fontWeight: FontWeight.w500, isCenter: true, isEnglishOnly: true)), + Expanded(flex: 2, child: (NumberFormat.decimalPattern().format(consultation.total) ?? '-').toText12(fontWeight: FontWeight.w500, isCenter: true, isEnglishOnly: true)), + ], + ), + ), + ), + ], + ), + ), + ), + ), + ], + ), ], ), ), @@ -279,7 +298,8 @@ class _MyInvoicesDetailsPageState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ LocaleKeys.amountBeforeTax.tr(context: context).toText14(isBold: true), - Utils.getPaymentAmountWithSymbol(widget.getInvoiceDetailsResponseModel.listConsultation!.first.totalShare.toString().toText16(isBold: true), AppColors.blackColor, 13, + Utils.getPaymentAmountWithSymbol( + NumberFormat.decimalPattern().format(widget.getInvoiceDetailsResponseModel.listConsultation!.first.totalShare).toString().toText16(isBold: true, isEnglishOnly: true), AppColors.blackColor, 13, isSaudiCurrency: true), ], ).paddingSymmetrical(24.h, 0.h), @@ -288,7 +308,12 @@ class _MyInvoicesDetailsPageState extends State { children: [ LocaleKeys.vat15.tr(context: context).toText14(isBold: true, color: AppColors.greyTextColor), Utils.getPaymentAmountWithSymbol( - widget.getInvoiceDetailsResponseModel.listConsultation!.first.totalVATAmount!.toString().toText14(isBold: true, color: AppColors.greyTextColor), AppColors.greyTextColor, 13, + NumberFormat.decimalPattern() + .format(widget.getInvoiceDetailsResponseModel.listConsultation!.first.totalVATAmount!) + .toString() + .toText14(isBold: true, color: AppColors.greyTextColor, isEnglishOnly: true), + AppColors.greyTextColor, + 13, isSaudiCurrency: true), ], ).paddingSymmetrical(24.h, 0.h), @@ -297,7 +322,11 @@ class _MyInvoicesDetailsPageState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ LocaleKeys.discount.tr(context: context).toText14(isBold: true), - Utils.getPaymentAmountWithSymbol(widget.getInvoiceDetailsResponseModel.listConsultation!.first.discountAmount!.toString().toText14(isBold: true, color: AppColors.primaryRedColor), + Utils.getPaymentAmountWithSymbol( + NumberFormat.decimalPattern() + .format(widget.getInvoiceDetailsResponseModel.listConsultation!.first.discountAmount!) + .toString() + .toText14(isBold: true, color: AppColors.primaryRedColor, isEnglishOnly: true), AppColors.primaryRedColor, 13, isSaudiCurrency: true), ], @@ -307,7 +336,9 @@ class _MyInvoicesDetailsPageState extends State { children: [ LocaleKeys.paid.tr(context: context).toText14(isBold: true), Utils.getPaymentAmountWithSymbol( - widget.getInvoiceDetailsResponseModel.listConsultation!.first.grandTotal!.toString().toText14(isBold: true, color: AppColors.textColor), AppColors.textColor, 13, + NumberFormat.decimalPattern().format(widget.getInvoiceDetailsResponseModel.listConsultation!.first.grandTotal!).toString().toText14(isBold: true, color: AppColors.textColor, isEnglishOnly: true), + AppColors.textColor, + 13, isSaudiCurrency: true), ], ).paddingSymmetrical(24.h, 0.h), diff --git a/lib/presentation/my_invoices/widgets/invoice_list_card.dart b/lib/presentation/my_invoices/widgets/invoice_list_card.dart index e5ae7de8..f2a1c6df 100644 --- a/lib/presentation/my_invoices/widgets/invoice_list_card.dart +++ b/lib/presentation/my_invoices/widgets/invoice_list_card.dart @@ -14,6 +14,8 @@ import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; +import 'dart:ui' as ui; + class InvoiceListCard extends StatelessWidget { final GetInvoicesListResponseModel getInvoicesListResponseModel; Function? onTap; @@ -94,7 +96,7 @@ class InvoiceListCard extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - (getIt().isArabic() ? getInvoicesListResponseModel.doctorNameN! : getInvoicesListResponseModel.doctorName!).toText16(isBold: true), + (getIt().isArabic() ? getInvoicesListResponseModel.doctorNameN! : getInvoicesListResponseModel.doctorName ?? "HMG Doctor").toText16(isBold: true), SizedBox(height: 8.h), Wrap( direction: Axis.horizontal, @@ -102,8 +104,9 @@ class InvoiceListCard extends StatelessWidget { runSpacing: 6.h, children: [ AppCustomChipWidget( - labelText: "${LocaleKeys.invoiceNo}: ${getInvoicesListResponseModel.invoiceNo!}", + labelText: "${LocaleKeys.invoiceNo.tr(context: context)}: ${getInvoicesListResponseModel.invoiceNo!}", labelPadding: EdgeInsetsDirectional.only(start: 6.w, end: 6.w), + isEnglishOnly: true, ), AppCustomChipWidget( labelText: @@ -114,10 +117,14 @@ class InvoiceListCard extends StatelessWidget { labelText: getInvoicesListResponseModel.projectName!, labelPadding: EdgeInsetsDirectional.only(start: 6.w, end: 6.w), ), - AppCustomChipWidget( - labelPadding: EdgeInsetsDirectional.only(start: -4.w, end: 6.w), - icon: AppAssets.doctor_calendar_icon, - labelText: DateUtil.formatDateToDate(DateUtil.convertStringToDate(getInvoicesListResponseModel.appointmentDate), false), + Directionality( + textDirection: ui.TextDirection.ltr, + child: AppCustomChipWidget( + labelPadding: EdgeInsetsDirectional.only(start: -4.w, end: 6.w), + icon: AppAssets.doctor_calendar_icon, + labelText: DateUtil.formatDateToDate(DateUtil.convertStringToDate(getInvoicesListResponseModel.appointmentDate), false), + isEnglishOnly: true, + ), ), ], ), diff --git a/lib/presentation/notifications/notifications_list_page.dart b/lib/presentation/notifications/notifications_list_page.dart index 6c344b2e..99201688 100644 --- a/lib/presentation/notifications/notifications_list_page.dart +++ b/lib/presentation/notifications/notifications_list_page.dart @@ -2,6 +2,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/utils/date_util.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; @@ -9,6 +10,7 @@ import 'package:hmg_patient_app_new/extensions/int_extensions.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/notifications/notifications_view_model.dart'; +import 'package:hmg_patient_app_new/features/todo_section/todo_section_view_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/lab/lab_result_item_view.dart'; import 'package:hmg_patient_app_new/presentation/notifications/notification_details_page.dart'; @@ -17,144 +19,115 @@ import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:provider/provider.dart'; import 'package:intl/intl.dart'; -class NotificationsListPage extends StatelessWidget { +class NotificationsListPage extends StatefulWidget { const NotificationsListPage({super.key}); + @override + State createState() => _NotificationsListPageState(); +} + +class _NotificationsListPageState extends State { + final GlobalKey _bottomKey = GlobalKey(); + + void _scrollToBottom() { + WidgetsBinding.instance.addPostFrameCallback((_) { + final ctx = _bottomKey.currentContext; + if (ctx != null) { + Scrollable.ensureVisible( + ctx, + duration: const Duration(milliseconds: 400), + curve: Curves.easeOut, + ); + } + }); + } + @override Widget build(BuildContext context) { return CollapsingListView( title: LocaleKeys.notification.tr(context: context), - child: SingleChildScrollView( - child: Consumer(builder: (context, notificationsVM, child) { - return Container( - margin: EdgeInsets.symmetric(vertical: 24.h), - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 24.h, - hasShadow: false, - ), - child: ListView.builder( - itemCount: notificationsVM.isNotificationsLoading ? 4 : notificationsVM.notificationsList.length, - physics: NeverScrollableScrollPhysics(), - shrinkWrap: true, - padding: EdgeInsetsGeometry.zero, - itemBuilder: (context, index) { - return notificationsVM.isNotificationsLoading - ? LabResultItemView( + child: Consumer(builder: (context, notificationsVM, child) { + return Column( + children: [ + Container( + margin: EdgeInsets.symmetric(vertical: 24.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.h, + hasShadow: false, + ), + child: ListView.builder( + itemCount: notificationsVM.isNotificationsLoading + ? 4 + : notificationsVM.notificationsList.length + (notificationsVM.hasMoreNotifications ? 1 : 0), + physics: NeverScrollableScrollPhysics(), + shrinkWrap: true, + padding: EdgeInsetsGeometry.zero, + itemBuilder: (context, index) { + if (notificationsVM.isNotificationsLoading) { + return LabResultItemView( onTap: () {}, labOrder: null, index: index, isLoading: true, - ) - : AnimationConfiguration.staggeredList( - position: index, - duration: const Duration(milliseconds: 500), - child: SlideAnimation( - verticalOffset: 100.0, - child: FadeInAnimation( - child: GestureDetector( - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => NotificationDetailsPage( - notification: notificationsVM.notificationsList[index], - ), - ), - ); - }, - child: AnimatedContainer( - duration: Duration(milliseconds: 300), - curve: Curves.easeInOut, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox(height: 16.h), - // Message row with red dot for unread - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: notificationsVM.notificationsList[index].message!.toText16( - isBold: (notificationsVM.notificationsList[index].isRead == false), - weight: (notificationsVM.notificationsList[index].isRead == false) - ? FontWeight.w600 - : FontWeight.w400, - ), + ); + } + return AnimationConfiguration.staggeredList( + position: index, + duration: const Duration(milliseconds: 500), + child: SlideAnimation( + verticalOffset: 100.0, + child: FadeInAnimation( + child: GestureDetector( + onTap: () { + notificationsVM.markAsRead(notificationID: notificationsVM.notificationsList[index].id ?? 0); + getIt.get().getPatientDashboard(); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => NotificationDetailsPage( + notification: notificationsVM.notificationsList[index], ), - SizedBox(width: 8.w), - // Red dot for unread notifications ONLY - if (notificationsVM.notificationsList[index].isRead == false) - Container( - width: 8.w, - height: 8.w, - decoration: BoxDecoration( - color: Colors.red, - shape: BoxShape.circle, - ), - ), - ], - ), - SizedBox(height: 12.h), - // First row: Time and Date chips with arrow - Row( + ), + ); + }, + child: AnimatedContainer( + duration: Duration(milliseconds: 300), + curve: Curves.easeInOut, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Time chip with clock icon - Container( - padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h), - decoration: BoxDecoration( - color: AppColors.greyColor, - borderRadius: BorderRadius.circular(8), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(Icons.access_time, size: 12.w, color: AppColors.textColor), - SizedBox(width: 4.w), - _formatTime(notificationsVM.notificationsList[index].isSentOn).toText10( - weight: FontWeight.w500, - color: AppColors.textColor, + SizedBox(height: 16.h), + // Message row with red dot for unread + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: notificationsVM.notificationsList[index].message!.toText16( + isBold: (notificationsVM.notificationsList[index].isRead == false), + weight: (notificationsVM.notificationsList[index].isRead == false) + ? FontWeight.w600 + : FontWeight.w400, ), - ], - ), - ), - SizedBox(width: 8.w), - // Date chip with calendar icon - Container( - padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h), - decoration: BoxDecoration( - color: AppColors.greyColor, - borderRadius: BorderRadius.circular(8), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(Icons.calendar_today, size: 12.w, color: AppColors.textColor), - SizedBox(width: 4.w), - _formatDate(notificationsVM.notificationsList[index].isSentOn).toText10( - weight: FontWeight.w500, - color: AppColors.textColor, + ), + SizedBox(width: 8.w), + // Red dot for unread notifications ONLY + if (notificationsVM.notificationsList[index].isRead == false) + Container( + width: 8.w, + height: 8.w, + decoration: BoxDecoration( + color: Colors.red, + shape: BoxShape.circle, + ), ), - ], - ), - ), - Spacer(), - // Arrow icon - Utils.buildSvgWithAssets( - icon: AppAssets.arrow_forward, - width: 16.w, - height: 16.h, - iconColor: AppColors.greyTextColor, + ], ), - ], - ), - // Second row: Contains Image chip (if MessageType is "image") - if (notificationsVM.notificationsList[index].messageType != null && - notificationsVM.notificationsList[index].messageType!.toLowerCase() == "image") - Padding( - padding: EdgeInsets.only(top: 8.h), - child: Row( + SizedBox(height: 12.h), + // First row: Time and Date chips with arrow + Row( children: [ + // Time chip with clock icon Container( padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h), decoration: BoxDecoration( @@ -164,31 +137,100 @@ class NotificationsListPage extends StatelessWidget { child: Row( mainAxisSize: MainAxisSize.min, children: [ - Icon(Icons.image_outlined, size: 12.w, color: AppColors.textColor), + Icon(Icons.access_time, size: 12.w, color: AppColors.textColor), SizedBox(width: 4.w), - 'Contains Image'.toText10( + _formatTime(notificationsVM.notificationsList[index].isSentOn).toText10( weight: FontWeight.w500, color: AppColors.textColor, ), ], ), ), + SizedBox(width: 8.w), + // Date chip with calendar icon + Container( + padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h), + decoration: BoxDecoration( + color: AppColors.greyColor, + borderRadius: BorderRadius.circular(8), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.calendar_today, size: 12.w, color: AppColors.textColor), + SizedBox(width: 4.w), + _formatDate(notificationsVM.notificationsList[index].isSentOn).toText10( + weight: FontWeight.w500, + color: AppColors.textColor, + ), + ], + ), + ), + Spacer(), + // Arrow icon + Utils.buildSvgWithAssets( + icon: AppAssets.arrow_forward, + width: 16.w, + height: 16.h, + iconColor: AppColors.greyTextColor, + ), ], ), - ), - SizedBox(height: 16.h), - 1.divider, - ], + // Second row: Contains Image chip (if MessageType is "image") + if (notificationsVM.notificationsList[index].messageType != null && + notificationsVM.notificationsList[index].messageType!.toLowerCase() == "image") + Padding( + padding: EdgeInsets.only(top: 8.h), + child: Row( + children: [ + Container( + padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h), + decoration: BoxDecoration( + color: AppColors.greyColor, + borderRadius: BorderRadius.circular(8), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.image_outlined, size: 12.w, color: AppColors.textColor), + SizedBox(width: 4.w), + 'Contains Image'.toText10( + weight: FontWeight.w500, + color: AppColors.textColor, + ), + ], + ), + ), + ], + ), + ), + SizedBox(height: 16.h), + 1.divider, + ], + ), + ), ), ), ), - ), - ), - ); - }).paddingSymmetrical(16.w, 0.h), - ).paddingSymmetrical(24.w, 0.h); + ); + }).paddingSymmetrical(16.w, 0.h), + ).paddingSymmetrical(24.w, 0.h), + SizedBox(height: 16.h), + // SizedBox( + // key: _bottomKey, + // child: "Show more notifications".toText16( + // color: AppColors.primaryRedColor, + // isBold: true, + // isUnderLine: true + // ), + // ).onPress(() async { + // await notificationsVM.loadMoreNotifications(); + // _scrollToBottom(); + // }), + SizedBox(height: 24.h), + ], + ); }), - ), ); } diff --git a/lib/presentation/parking/paking_page.dart b/lib/presentation/parking/paking_page.dart index db451b27..7673d15f 100644 --- a/lib/presentation/parking/paking_page.dart +++ b/lib/presentation/parking/paking_page.dart @@ -1,7 +1,10 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; @@ -103,6 +106,18 @@ class _ParkingPageState extends State { child: Padding( padding: EdgeInsets.all(16.h), child: LocaleKeys.parkingDescription.tr(context: context).toText12(fontWeight: FontWeight.w500, color: AppColors.textColor)), ).paddingOnly(top: 16, bottom: 16), + ClipRRect( + borderRadius: BorderRadius.circular(24.r), + child: Transform.flip( + flipX: getIt.get().isArabic(), + child: Image.asset( + AppAssets.carParkingService, + fit: BoxFit.fitHeight, + height: 480.h, + width: 520.w, + ), + ), + ), ], ), ), diff --git a/lib/presentation/prescriptions/prescription_delivery_order_summary_page.dart b/lib/presentation/prescriptions/prescription_delivery_order_summary_page.dart index d6a616ee..070132ad 100644 --- a/lib/presentation/prescriptions/prescription_delivery_order_summary_page.dart +++ b/lib/presentation/prescriptions/prescription_delivery_order_summary_page.dart @@ -135,6 +135,7 @@ class PrescriptionDeliveryOrderSummaryPage extends StatelessWidget { isCloseButtonVisible: true, isDismissible: false, isFullScreen: false, + isAutoDismiss: true ); }, onError: (err) { diff --git a/lib/presentation/prescriptions/prescription_detail_page.dart b/lib/presentation/prescriptions/prescription_detail_page.dart index 0ac9ce40..0d7d373a 100644 --- a/lib/presentation/prescriptions/prescription_detail_page.dart +++ b/lib/presentation/prescriptions/prescription_detail_page.dart @@ -154,6 +154,7 @@ class _PrescriptionDetailPageState extends State { icon: AppAssets.rating_icon, iconColor: AppColors.ratingColorYellow, labelText: LocaleKeys.ratingValue.tr(namedArgs: {'rating': widget.prescriptionsResponseModel.decimalDoctorRate.toString()}, context: context), + isEnglishOnly: true, ), AppCustomChipWidget( labelText: widget.prescriptionsResponseModel.name ?? "", diff --git a/lib/presentation/prescriptions/prescription_item_view.dart b/lib/presentation/prescriptions/prescription_item_view.dart index 7f969b21..5b52b99b 100644 --- a/lib/presentation/prescriptions/prescription_item_view.dart +++ b/lib/presentation/prescriptions/prescription_item_view.dart @@ -13,6 +13,7 @@ import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; +import 'package:hmg_patient_app_new/widgets/scroll_wheel_time_picker.dart'; class PrescriptionItemView extends StatelessWidget { int index; @@ -67,7 +68,7 @@ class PrescriptionItemView extends StatelessWidget { labelText: "${LocaleKeys.dailyDoses.tr(context: context)}: ${isLoading ? "" : prescriptionVM.prescriptionDetailsList[index].doseDailyQuantity}", ).toShimmer2(isShow: isLoading), AppCustomChipWidget( - labelText: "${LocaleKeys.days.tr(context: context)}: ${isLoading ? "" : prescriptionVM.prescriptionDetailsList[index].days}", + labelText: "${LocaleKeys.days.tr(context: context)}: ${isLoading ? "" : prescriptionVM.prescriptionDetailsList[index].days}", isEnglishOnly: true, ).toShimmer2(isShow: isLoading), ], ).paddingSymmetrical(16.h, 0.h), @@ -110,51 +111,60 @@ class PrescriptionItemView extends StatelessWidget { if (await prescriptionVM.checkIfReminderExistForPrescription(index)) { prescriptionVM.prescriptionDetailsList[index].hasReminder = true; - if (prescriptionVM.prescriptionDetailsList[index].hasReminder ?? false) { + LoaderBottomSheet.showLoader(loadingText: "Removing Reminders"); bool resultValue = await calender.checkAndRemoveMultipleItems(id: prescriptionVM.prescriptionDetailsList[index].itemID.toString()); prescriptionVM.setPrescriptionItemReminder(newValue, prescriptionVM.prescriptionDetailsList[index]); LoaderBottomSheet.hideLoader(); return; - } + } else { DateTime startDate = DateTime.now(); + prescriptionVM.serSelectedTime(startDate); DateTime endDate = DateTime(startDate.year, startDate.month, startDate.day + prescriptionVM.prescriptionDetailsList[index].days!.toInt()); - BottomSheetUtils().showReminderBottomSheet( + showCommonBottomSheetWithoutHeight( context, - endDate, - "", - prescriptionVM.prescriptionDetailsList[index].itemID.toString(), - "", - "", - title: "${prescriptionVM.prescriptionDetailsList[index].itemDescription} Prescription Reminder", - description: - "${prescriptionVM.prescriptionDetailsList[index].itemDescription} ${prescriptionVM.prescriptionDetailsList[index].frequency} ${prescriptionVM.prescriptionDetailsList[index].route} ", - onSuccess: () {}, - isMultiAllowed: true, - onMultiDateSuccess: (int selectedIndex) async { - bool isEventAdded = await calender.createMultipleEvents( - reminderMinutes: selectedIndex, - frequencyNumber: prescriptionVM.prescriptionDetailsList[index].frequencyNumber?.toInt(), - days: prescriptionVM.prescriptionDetailsList[index].days!.toInt(), - orderDate: prescriptionVM.prescriptionDetailsList[index].orderDate!, - itemDescriptionN: prescriptionVM.prescriptionDetailsList[index].itemDescription!, - route: prescriptionVM.prescriptionDetailsList[index].route!, - onFailure: (errorMessage) => prescriptionVM.showError(errorMessage), - prescriptionNumber: prescriptionVM.prescriptionDetailsList[index].itemID.toString(), - ); - prescriptionVM.setPrescriptionItemReminder(isEventAdded, prescriptionVM.prescriptionDetailsList[index]); - // setCalender(context, - // eventId: prescriptionVM.prescriptionDetailsList[index].itemID.toString(), - // selectedMinutes: selectedIndex, - // frequencyNumber: prescriptionVM.prescriptionDetailsList[index].frequencyNumber?.toInt(), - // days: prescriptionVM.prescriptionDetailsList[index].days!.toInt(), - // orderDate: prescriptionVM.prescriptionDetailsList[index].orderDate!, - // itemDescriptionN: prescriptionVM.prescriptionDetailsList[index].itemDescription!, - // route: prescriptionVM.prescriptionDetailsList[index].route!); - }, + child: bottomSheetContent(context), + title: LocaleKeys.timeForFirstReminder.tr(), + + isCloseButtonVisible: true ); + + // BottomSheetUtils().showReminderBottomSheet( + // context, + // endDate, + // "", + // prescriptionVM.prescriptionDetailsList[index].itemID.toString(), + // "", + // "", + // title: "${prescriptionVM.prescriptionDetailsList[index].itemDescription} Prescription Reminder", + // description: + // "${prescriptionVM.prescriptionDetailsList[index].itemDescription} ${prescriptionVM.prescriptionDetailsList[index].frequency} ${prescriptionVM.prescriptionDetailsList[index].route} ", + // onSuccess: () {}, + // isMultiAllowed: true, + // onMultiDateSuccess: (int selectedIndex) async { + // bool isEventAdded = await calender.createMultipleEvents( + // reminderMinutes: selectedIndex, + // frequencyNumber: prescriptionVM.prescriptionDetailsList[index].frequencyNumber?.toInt(), + // days: prescriptionVM.prescriptionDetailsList[index].days!.toInt(), + // orderDate: prescriptionVM.prescriptionDetailsList[index].orderDate!, + // itemDescriptionN: prescriptionVM.prescriptionDetailsList[index].itemDescription!, + // route: prescriptionVM.prescriptionDetailsList[index].route!, + // onFailure: (errorMessage) => prescriptionVM.showError(errorMessage), + // prescriptionNumber: prescriptionVM.prescriptionDetailsList[index].itemID.toString(), + // ); + // prescriptionVM.setPrescriptionItemReminder(isEventAdded, prescriptionVM.prescriptionDetailsList[index]); + // // setCalender(context, + // // eventId: prescriptionVM.prescriptionDetailsList[index].itemID.toString(), + // // selectedMinutes: selectedIndex, + // // frequencyNumber: prescriptionVM.prescriptionDetailsList[index].frequencyNumber?.toInt(), + // // days: prescriptionVM.prescriptionDetailsList[index].days!.toInt(), + // // orderDate: prescriptionVM.prescriptionDetailsList[index].orderDate!, + // // itemDescriptionN: prescriptionVM.prescriptionDetailsList[index].itemDescription!, + // // route: prescriptionVM.prescriptionDetailsList[index].route!); + // }, + // ); } }, ).toShimmer2(isShow: isLoading), @@ -186,4 +196,47 @@ class PrescriptionItemView extends StatelessWidget { ), ); } + + Widget bottomSheetContent(BuildContext context) { + return Column( + children: [ + SizedBox(height: 4.h), + LocaleKeys.reminderRemovalNote.tr().toText14(isBold: true, color: AppColors.warningColorYellow), + SizedBox(height: 8.h), + ScrollWheelTimePicker( + initialHour: DateTime.now().hour, + initialMinute: DateTime.now().minute, + use24HourFormat: true, + pickerHeight: 200.h, + itemExtent: 100.h, + onTimeChanged: (time) { + // Handle selected time + debugPrint('Selected time: ${time.hour}:${time.minute}'); + prescriptionVM.serSelectedTime(DateTime(DateTime.now().year, DateTime.now().month, DateTime.now().day, time.hour, time.minute)); + }, + ), + SizedBox(height: 8.h), + CustomButton( + backgroundColor: AppColors.successColor, + borderColor: AppColors.successColor, + text: LocaleKeys.confirm.tr(), onPressed: () async { + CalenderUtilsNew calender = CalenderUtilsNew.instance; + bool isEventAdded = await calender.createMultipleEvents( + reminderMinutes: 10, + frequencyNumber: prescriptionVM.prescriptionDetailsList[index].frequencyNumber?.toInt(), + days: prescriptionVM.prescriptionDetailsList[index].days!.toInt(), + orderDate: prescriptionVM.prescriptionDetailsList[index].orderDate!, + itemDescriptionN: prescriptionVM.prescriptionDetailsList[index].itemDescription!, + route: prescriptionVM.prescriptionDetailsList[index].route!, + onFailure: (errorMessage) => prescriptionVM.showError(errorMessage), + prescriptionNumber: prescriptionVM.prescriptionDetailsList[index].itemID.toString(), + scheduleDateTime: prescriptionVM.selectedReminderTime, + ); + prescriptionVM.setPrescriptionItemReminder(isEventAdded, prescriptionVM.prescriptionDetailsList[index]); + Navigator.of(context).pop(); + }) + + ], + ); + } } diff --git a/lib/presentation/prescriptions/prescription_reminder_view.dart b/lib/presentation/prescriptions/prescription_reminder_view.dart index aab587d4..d1dcd07a 100644 --- a/lib/presentation/prescriptions/prescription_reminder_view.dart +++ b/lib/presentation/prescriptions/prescription_reminder_view.dart @@ -20,7 +20,7 @@ class PrescriptionReminderView extends StatefulWidget { } class _PrescriptionReminderViewState extends State { - final List _options = [15, 30, 60, 90]; + final List _options = [60, 360, 720, 1440]; int _selectedOption = 0; // Nullable to represent no selection initially @override @@ -54,7 +54,7 @@ class _PrescriptionReminderViewState extends State { ), child: RadioListTile( title: Text( - "${_options[index]} ${LocaleKeys.minute.tr(context: context)}", + "${(_options[index] / 60).toInt()} ${LocaleKeys.hours.tr(context: context)}", style: TextStyle( fontSize: 16.h, fontWeight: FontWeight.w500, diff --git a/lib/presentation/profile_settings/profile_settings.dart b/lib/presentation/profile_settings/profile_settings.dart index 1a10e0de..fd88c049 100644 --- a/lib/presentation/profile_settings/profile_settings.dart +++ b/lib/presentation/profile_settings/profile_settings.dart @@ -28,6 +28,7 @@ import 'package:hmg_patient_app_new/presentation/habib_wallet/habib_wallet_page. import 'package:hmg_patient_app_new/presentation/habib_wallet/recharge_wallet_page.dart'; import 'package:hmg_patient_app_new/presentation/insurance/widgets/insurance_update_details_card.dart'; import 'package:hmg_patient_app_new/presentation/profile_settings/widgets/update_email_widget.dart'; +import 'package:hmg_patient_app_new/presentation/profile_settings/widgets/update_emergency_contact_widget.dart'; import 'package:hmg_patient_app_new/services/dialog_service.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/app_language_change.dart'; @@ -39,6 +40,7 @@ import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher.dart'; +import 'package:in_app_review/in_app_review.dart'; class ProfileSettings extends StatefulWidget { const ProfileSettings({super.key}); @@ -48,6 +50,9 @@ class ProfileSettings extends StatefulWidget { } class ProfileSettingsState extends State { + + final InAppReview inAppReview = InAppReview.instance; + @override void initState() { scheduleMicrotask(() { @@ -242,49 +247,47 @@ class ProfileSettingsState extends State { ], ), ), - // Container( - // margin: EdgeInsets.only(left: 24.w, right: 24.w, top: 16.h, bottom: 24.h), - // padding: EdgeInsets.only(top: 4.h, bottom: 4.h), - // decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r, hasShadow: true), - // child: Column( - // children: [ - // actionItem(AppAssets.language_change, LocaleKeys.language.tr(context: context), () { - // showCommonBottomSheetWithoutHeight(context, title: LocaleKeys.language.tr(context: context), child: AppLanguageChange(), callBackFunc: () {}, isFullScreen: false); - // }, trailingLabel: Utils.appState.isArabic() ? "العربية" : "English"), - // 1.divider, - // actionItem(AppAssets.bell, LocaleKeys.notificationsSettings.tr(context: context), () { - // openAppSettings(); - // }), - // // 1.divider, - // // actionItem(AppAssets.touch_face_id, LocaleKeys.touchIDFaceIDServices.tr(context: context), () {}, switchValue: true), - // ], - // ), - // ), - // LocaleKeys.personalInformation.tr(context: context).toText18(weight: FontWeight.w600, textOverflow: TextOverflow.ellipsis, maxlines: 1).paddingOnly(left: 24.w, right: 24.w), - // Container( - // margin: EdgeInsets.only(left: 24.w, right: 24.w, top: 16.h, bottom: 24.h), - // padding: EdgeInsets.only(top: 4.h, bottom: 4.h), - // decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r, hasShadow: true), - // child: Column( - // children: [ - // actionItem(AppAssets.email_transparent, LocaleKeys.updateEmailAddress.tr(context: context), () { - // showCommonBottomSheetWithoutHeight( - // context, - // title: LocaleKeys.updateEmailAddress.tr(context: context), - // child: UpdateEmailDialog(), - // callBackFunc: () {}, - // isFullScreen: false, - // ); - // }), - // // 1.divider, - // // actionItem(AppAssets.smart_phone_fill, "Phone Number".needTranslation, () {}), - // // 1.divider, - // // actionItem(AppAssets.my_address, "My Addresses".needTranslation, () {}), - // // 1.divider, - // // actionItem(AppAssets.emergency, "Emergency Contact".needTranslation, () {}), - // ], - // ), - // ), + LocaleKeys.personalInformation.tr(context: context).toText18(weight: FontWeight.w600, textOverflow: TextOverflow.ellipsis, maxlines: 1).paddingOnly(left: 24.w, right: 24.w), + Container( + margin: EdgeInsets.only(left: 24.w, right: 24.w, top: 16.h, bottom: 24.h), + padding: EdgeInsets.only(top: 4.h, bottom: 4.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r, hasShadow: true), + child: Column( + children: [ + actionItem(AppAssets.email_transparent, LocaleKeys.updateEmailAddress.tr(context: context), () { + showCommonBottomSheetWithoutHeight( + context, + title: LocaleKeys.updateEmailAddress.tr(context: context), + child: UpdateEmailDialog(), + callBackFunc: () {}, + isFullScreen: false, + ); + }), + 1.divider, + actionItem(AppAssets.language, LocaleKeys.communicationLanguage.tr(context: context), () { + profileVm.openPreferredLanguageBottomSheet(); + }, + trailingLabel: profileVm.isPatientProfileLoading + ? "English" + : (profileVm.getPatientInfoForUpdate.preferredLanguage! == "1" ? LocaleKeys.arabic.tr(context: context) : LocaleKeys.english.tr(context: context)), + isShowLoading: profileVm.isPatientProfileLoading), + 1.divider, + actionItem(AppAssets.smart_phone_fill, LocaleKeys.emrgNo.tr(context: context), () { + showCommonBottomSheetWithoutHeight( + context, + title: LocaleKeys.emrgNo.tr(context: context), + child: UpdateEmergencyContactDialog(), + callBackFunc: () {}, + isFullScreen: false, + ); + }), + // 1.divider, + // actionItem(AppAssets.my_address, "My Addresses".needTranslation, () {}), + // 1.divider, + // actionItem(AppAssets.emergency, "Emergency Contact".needTranslation, () {}), + ], + ), + ), LocaleKeys.helpAndSupport.tr(context: context).toText18(weight: FontWeight.w600, textOverflow: TextOverflow.ellipsis, maxlines: 1).paddingOnly(left: 24.w, right: 24.w), Container( margin: EdgeInsets.only(left: 24.w, right: 24.w, top: 16.h), @@ -314,17 +317,23 @@ class ProfileSettingsState extends State { 1.divider, // actionItem(AppAssets.permission, LocaleKeys.permissions.tr(context: context), () {}, trailingLabel: "Location, Camera"), // 1.divider, - actionItem(AppAssets.rate, LocaleKeys.rateApp.tr(context: context), () { - if (Platform.isAndroid) { - Utils.openWebView( - url: 'https://play.google.com/store/apps/details?id=com.ejada.hmg', - ); + actionItem(AppAssets.rate, LocaleKeys.rateApp.tr(context: context), () async { + if (await inAppReview.isAvailable()) { + inAppReview.requestReview(); } else { - Utils.openWebView( - url: 'https://itunes.apple.com/app/id733503978', - ); + inAppReview.openStoreListing(appStoreId: '6758851027'); } - }, isExternalLink: true), + // if (Platform.isAndroid) { + // Utils.openWebView( + // url: 'https://play.google.com/store/apps/details?id=com.ejada.hmg', + // ); + // } else { + // Utils.openWebView( + // url: 'https://itunes.apple.com/app/id733503978', + // ); + // } + // }, isExternalLink: true), + }), 1.divider, actionItem(AppAssets.privacy_terms, LocaleKeys.privacyPolicy.tr(context: context), () { Utils.openWebView( @@ -340,12 +349,12 @@ class ProfileSettingsState extends State { ], ), ), - CustomButton( - height: 56.h, - icon: AppAssets.minus, - text: LocaleKeys.deactivateAccount.tr(context: context), - onPressed: () {}, - ).paddingAll(24.w), + // CustomButton( + // height: 56.h, + // icon: AppAssets.minus, + // text: LocaleKeys.deactivateAccount.tr(context: context), + // onPressed: () {}, + // ).paddingAll(24.w), ], ); }, @@ -382,7 +391,8 @@ class ProfileSettingsState extends State { return _permissionsLabel; } - Widget actionItem(String icon, String label, VoidCallback onPress, {String trailingLabel = "", bool? switchValue, ValueChanged? onSwitchChanged, bool isExternalLink = false}) { + Widget actionItem(String icon, String label, VoidCallback onPress, + {String trailingLabel = "", bool? switchValue, ValueChanged? onSwitchChanged, bool isExternalLink = false, bool isShowLoading = false}) { return SizedBox( height: 56.h, child: Row( @@ -390,7 +400,11 @@ class ProfileSettingsState extends State { children: [ Utils.buildSvgWithAssets(icon: icon, iconColor: AppColors.greyTextColor), label.toText14(weight: FontWeight.w500, textOverflow: TextOverflow.ellipsis, maxlines: 1).expanded, - if (trailingLabel.isNotEmpty) trailingLabel.toText14(color: AppColors.greyTextColor, weight: FontWeight.w500, textOverflow: TextOverflow.ellipsis, maxlines: 1), + if (trailingLabel.isNotEmpty) + ConstrainedBox( + constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.35), + child: trailingLabel.toText14(color: AppColors.greyTextColor, weight: FontWeight.w500, textOverflow: TextOverflow.ellipsis, maxlines: 1).toShimmer2(isShow: isShowLoading), + ), switchValue != null ? Switch( value: switchValue, diff --git a/lib/presentation/profile_settings/widgets/preferred_language_widget.dart b/lib/presentation/profile_settings/widgets/preferred_language_widget.dart new file mode 100644 index 00000000..9678349b --- /dev/null +++ b/lib/presentation/profile_settings/widgets/preferred_language_widget.dart @@ -0,0 +1,116 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/int_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/profile_settings/profile_settings_view_model.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; +import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; +import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; +import 'package:provider/provider.dart'; + +class PreferredLanguageWidget extends StatefulWidget { + PreferredLanguageWidget({super.key}); + + @override + State createState() => _PreferredLanguageWidgetState(); +} + +class _PreferredLanguageWidgetState extends State { + String? selectedValue; + late ProfileSettingsViewModel profileSettingsViewModel; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + setState(() { + selectedValue = profileSettingsViewModel.getPatientInfoForUpdate.preferredLanguage!; + }); + }); + } + + @override + Widget build(BuildContext context) { + profileSettingsViewModel = Provider.of(context, listen: false); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 24.h, + children: [ + LocaleKeys.communicationLanguage.tr(context: context).toText14(), + Container( + padding: EdgeInsets.only(top: 4, bottom: 4), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.h, hasShadow: true), + child: Column( + children: [ + languageItem("English", "2"), + 1.divider, + languageItem("العربية", "1"), + ], + ), + ), + CustomButton( + text: LocaleKeys.save.tr(context: context), + onPressed: () { + LoaderBottomSheet.showLoader(loadingText: LocaleKeys.loadingText.tr(context: context)); + profileSettingsViewModel!.updatePatientInfo( + patientInfo: { + "PreferredLanguage": selectedValue, + "EmailAddress": profileSettingsViewModel.getPatientInfoForUpdate.emailAddress, + "EmergencyContactName": profileSettingsViewModel.getPatientInfoForUpdate.emergencyContactName, + "EmergencyContactNo": profileSettingsViewModel.getPatientInfoForUpdate.emergencyContactNo, + "IsEmailAlertRequired": true, + "IsSMSAlertRequired": true, + }, + onSuccess: (response) { + LoaderBottomSheet.hideLoader(); + showCommonBottomSheetWithoutHeight(context, title: LocaleKeys.success.tr(context: context), child: Utils.getSuccessWidget(loadingText: LocaleKeys.success.tr()), + callBackFunc: () async { + Navigator.of(context).pop(); + profileSettingsViewModel.getProfileSettings(); + }, isFullScreen: false, isAutoDismiss: true); + }, + onError: (error) { + LoaderBottomSheet.hideLoader(); + // Show error message + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(error)), + ); + }, + ); + }), + ], + ); + } + + Widget languageItem(String title, String _value) { + return SizedBox( + height: 72, + child: Row( + spacing: 8.h, + children: [ + Radio( + value: _value, + groupValue: selectedValue, + activeColor: AppColors.errorColor, + onChanged: (value) { + setState(() { + selectedValue = _value; + }); + }, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + title.toText16(weight: FontWeight.w500, textOverflow: TextOverflow.ellipsis, maxlines: 1, fontFamily: _value == "1" ? 'GESSTwo' : 'Poppins').expanded, + ], + ).paddingOnly(left: 16, right: 16).onPress(() { + setState(() { + selectedValue = _value; + }); + }), + ); + } +} diff --git a/lib/presentation/profile_settings/widgets/update_email_widget.dart b/lib/presentation/profile_settings/widgets/update_email_widget.dart index ad9b9d5a..0b27dc82 100644 --- a/lib/presentation/profile_settings/widgets/update_email_widget.dart +++ b/lib/presentation/profile_settings/widgets/update_email_widget.dart @@ -31,7 +31,11 @@ class _UpdateEmailDialogState extends State { void initState() { _textFieldFocusNode = FocusNode(); textController = TextEditingController(); - textController!.text = getIt.get().getAuthenticatedUser()!.emailAddress ?? ""; + WidgetsBinding.instance.addPostFrameCallback((_) { + setState(() { + textController!.text = profileSettingsViewModel!.getPatientInfoForUpdate.emailAddress ?? ""; + }); + }); super.initState(); } @@ -52,7 +56,7 @@ class _UpdateEmailDialogState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - "Enter the new email address to be updated in your HMG File: ".toText16(textAlign: TextAlign.start, weight: FontWeight.w500), + "Enter the new email address to be updated: ".toText16(textAlign: TextAlign.start, weight: FontWeight.w500), SizedBox(height: 12.h), TextInputWidget( labelText: LocaleKeys.email.tr(), @@ -78,13 +82,21 @@ class _UpdateEmailDialogState extends State { onPressed: () { LoaderBottomSheet.showLoader(loadingText: LocaleKeys.updatingEmailAddress.tr(context: context)); profileSettingsViewModel!.updatePatientInfo( - patientInfo: {"EmailAddress": textController!.text}, + patientInfo: { + "PreferredLanguage": profileSettingsViewModel!.getPatientInfoForUpdate.preferredLanguage, + "EmailAddress": textController!.text, + "EmergencyContactName": profileSettingsViewModel!.getPatientInfoForUpdate.emergencyContactName, + "EmergencyContactNo": profileSettingsViewModel!.getPatientInfoForUpdate.emergencyContactNo, + "IsEmailAlertRequired": true, + "IsSMSAlertRequired": true, + }, onSuccess: (response) { LoaderBottomSheet.hideLoader(); showCommonBottomSheetWithoutHeight(context, title: LocaleKeys.success.tr(context: context), child: Utils.getSuccessWidget(loadingText: LocaleKeys.success.tr()), callBackFunc: () async { Navigator.of(context).pop(); - }, isFullScreen: false); + profileSettingsViewModel!.getProfileSettings(); + }, isFullScreen: false, isAutoDismiss: true); }, onError: (error) { LoaderBottomSheet.hideLoader(); diff --git a/lib/presentation/profile_settings/widgets/update_emergency_contact_widget.dart b/lib/presentation/profile_settings/widgets/update_emergency_contact_widget.dart new file mode 100644 index 00000000..71505dff --- /dev/null +++ b/lib/presentation/profile_settings/widgets/update_emergency_contact_widget.dart @@ -0,0 +1,118 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/features/profile_settings/profile_settings_view_model.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; +import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; +import 'package:hmg_patient_app_new/widgets/input_widget.dart'; +import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; +import 'package:provider/provider.dart'; + +class UpdateEmergencyContactDialog extends StatefulWidget { + UpdateEmergencyContactDialog({super.key}); + + @override + State createState() => _UpdateEmergencyContactDialogState(); +} + +class _UpdateEmergencyContactDialogState extends State { + late FocusNode _textFieldFocusNode; + late TextEditingController? textController; + ProfileSettingsViewModel? profileSettingsViewModel; + + @override + void initState() { + _textFieldFocusNode = FocusNode(); + textController = TextEditingController(); + WidgetsBinding.instance.addPostFrameCallback((_) { + setState(() { + textController!.text = profileSettingsViewModel!.getPatientInfoForUpdate.emergencyContactNo!; + }); + }); + super.initState(); + } + + @override + void dispose() { + _textFieldFocusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + profileSettingsViewModel = Provider.of(context); + return GestureDetector( + onTap: () { + _textFieldFocusNode.unfocus(); + FocusScope.of(context).unfocus(); + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "Enter the new emergency contact number to be updated: ".toText16(textAlign: TextAlign.start, weight: FontWeight.w500), + SizedBox(height: 12.h), + TextInputWidget( + labelText: LocaleKeys.emrgNo.tr(), + hintText: "05xxxxxxxx", + controller: textController, + focusNode: _textFieldFocusNode, + autoFocus: true, + padding: EdgeInsets.all(8.h), + keyboardType: TextInputType.number, + isEnable: true, + isReadOnly: false, + prefix: null, + isBorderAllowed: false, + isAllowLeadingIcon: true, + fontSize: 14.f, + isCountryDropDown: false, + leadingIcon: AppAssets.call_fill, + fontFamily: "Poppins", + ), + SizedBox(height: 12.h), + CustomButton( + text: LocaleKeys.submit.tr(context: context), + onPressed: () { + LoaderBottomSheet.showLoader(loadingText: LocaleKeys.loadingText.tr(context: context)); + profileSettingsViewModel!.updatePatientInfo( + patientInfo: { + "PreferredLanguage": profileSettingsViewModel!.getPatientInfoForUpdate.preferredLanguage, + "EmailAddress": profileSettingsViewModel!.getPatientInfoForUpdate.emailAddress, + "EmergencyContactName": profileSettingsViewModel!.getPatientInfoForUpdate.emergencyContactName, + "EmergencyContactNo": textController!.text, + "IsEmailAlertRequired": true, + "IsSMSAlertRequired": true, + }, + onSuccess: (response) { + LoaderBottomSheet.hideLoader(); + showCommonBottomSheetWithoutHeight(context, title: LocaleKeys.success.tr(context: context), child: Utils.getSuccessWidget(loadingText: LocaleKeys.success.tr()), + callBackFunc: () async { + Navigator.of(context).pop(); + profileSettingsViewModel!.getProfileSettings(); + }, isFullScreen: false, isAutoDismiss: true); + }, + onError: (error) { + LoaderBottomSheet.hideLoader(); + // Show error message + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(error)), + ); + }, + ); + }, + backgroundColor: AppColors.primaryRedColor, + borderColor: AppColors.primaryRedColor, + textColor: const Color(0xFFffffff), + ), + ], + ), + ); + } +} diff --git a/lib/presentation/radiology/radiology_orders_page.dart b/lib/presentation/radiology/radiology_orders_page.dart index 1a3ab448..db2e4f52 100644 --- a/lib/presentation/radiology/radiology_orders_page.dart +++ b/lib/presentation/radiology/radiology_orders_page.dart @@ -11,7 +11,6 @@ import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/lab/lab_view_model.dart'; -import 'package:hmg_patient_app_new/features/radiology/models/resp_models/patient_radiology_response_model.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; import 'package:hmg_patient_app_new/presentation/lab/lab_result_item_view.dart'; import 'package:hmg_patient_app_new/presentation/radiology/radiology_result_page.dart'; diff --git a/lib/presentation/radiology/radiology_result_page.dart b/lib/presentation/radiology/radiology_result_page.dart index 05f23530..8fbdd8e6 100644 --- a/lib/presentation/radiology/radiology_result_page.dart +++ b/lib/presentation/radiology/radiology_result_page.dart @@ -158,28 +158,64 @@ class _RadiologyResultPageState extends State { borderRadius: 24.h, hasShadow: true, ), - child: widget.patientRadiologyResponseModel.dIAPACSURL != "" ? CustomButton( - text: LocaleKeys.openRad.tr(context: context), - onPressed: () async { - if (radiologyViewModel.radiologyImageURL.isNotEmpty) { - Uri uri = Uri.parse(radiologyViewModel.radiologyImageURL); - launchUrl(uri, mode: LaunchMode.platformDefault, webOnlyWindowName: ""); - } else { - Utils.showToast("Radiology image not available"); - } - }, - backgroundColor: AppColors.primaryRedColor, - borderColor: AppColors.primaryRedColor, - textColor: Colors.white, - fontSize: 16, - fontWeight: FontWeight.w500, - borderRadius: 12, - padding: EdgeInsets.fromLTRB(10, 0, 10, 0), - height: 45.h, - icon: AppAssets.imageIcon, - iconColor: Colors.white, - iconSize: 20.h, - ).paddingSymmetrical(24.h, 24.h) : SizedBox.shrink(), + child: Column( + children: [ + SizedBox(height: 24.h), + widget.patientRadiologyResponseModel.dIAPACSURL != "" + ? CustomButton( + text: LocaleKeys.openRad.tr(context: context), + onPressed: () async { + if (radiologyViewModel.radiologyImageURL.isNotEmpty) { + Uri uri = Uri.parse(radiologyViewModel.radiologyImageURL); + launchUrl(uri, mode: LaunchMode.platformDefault, webOnlyWindowName: ""); + } else { + Utils.showToast("Radiology image not available"); + } + }, + backgroundColor: AppColors.primaryRedColor, + borderColor: AppColors.primaryRedColor, + textColor: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w500, + borderRadius: 12, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 56.h, + icon: AppAssets.imageIcon, + iconColor: Colors.white, + iconSize: 20.h, + ).paddingSymmetrical(24.h, 0.h) + : SizedBox.shrink(), + Container( + height: 56.h, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12.r), + gradient: LinearGradient( + begin: Alignment.centerLeft, + end: Alignment.centerRight, + stops: [0.236, 1.0], // 53.6% and 100% + colors: [ + Color(0xFF8A38F5), // Transparent + Color(0xFFE20BBB), // Solid #F8F8F8 + ], + ), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: EdgeInsets.only(right: 4.w, left: 4.w), + child: Utils.buildSvgWithAssets(icon: AppAssets.aiOverView, width: 16.h, height: 16.h, iconColor: Colors.white), + ), + LocaleKeys.generateAiAnalysisResult.tr(context: context).toText16(isBold: true, color: Colors.white) + ], + ), + ).paddingSymmetrical(24.h, 12.h).onPress(() { + // Need to implement the Radiology AI analysis result generation functionality here + Utils.showToast("Radiology AI analysis result generation functionality is not implemented yet"); + }), + ], + ), ), ], ), diff --git a/lib/presentation/rate_appointment/rate_appointment_doctor.dart b/lib/presentation/rate_appointment/rate_appointment_doctor.dart index b9a6c587..dfc15cd3 100644 --- a/lib/presentation/rate_appointment/rate_appointment_doctor.dart +++ b/lib/presentation/rate_appointment/rate_appointment_doctor.dart @@ -10,6 +10,7 @@ import 'package:hmg_patient_app_new/presentation/rate_appointment/rate_appointme import 'package:hmg_patient_app_new/presentation/rate_appointment/widget/doctor_row.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; +import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; import 'package:provider/provider.dart'; class RateAppointmentDoctor extends StatefulWidget { @@ -25,7 +26,7 @@ class RateAppointmentDoctor extends StatefulWidget { class _RateAppointmentDoctorState extends State { final formKey = GlobalKey(); String note = ""; - int rating = 5; + int rating = 0; // ProjectViewModel? projectViewModel; AppointmentRatingViewModel? appointmentRatingViewModel; @@ -48,16 +49,16 @@ class _RateAppointmentDoctorState extends State { return Selector( selector: (_, vm) => vm.isRateClinic, - builder: (context, isRateClinic, child) => isRateClinic - ? RateAppointmentClinic(doctorNote: note, doctorRate: rating,) - : SizedBox( - height: sheetHeight, + builder: (context, isRateClinic, child) => + // isRateClinic ? RateAppointmentClinic(doctorNote: note, doctorRate: rating,) + // : + SizedBox( + height: sheetHeight, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Scrollable main content Expanded( - child: Padding( padding: const EdgeInsets.only(top: 0.0, left: 0, right: 0), child: Column( @@ -144,7 +145,7 @@ class _RateAppointmentDoctorState extends State { decoration: InputDecoration.collapsed( hintText: LocaleKeys.notes.tr(context: context), hintStyle: TextStyle( - fontSize: 16, + fontSize: 16.f, fontWeight: FontWeight.w600, color: Color(0xff2B353E), letterSpacing: -0.64, @@ -162,7 +163,6 @@ class _RateAppointmentDoctorState extends State { ), ), - // Bottom action buttons pinned to bottom SafeArea( top: false, @@ -172,7 +172,7 @@ class _RateAppointmentDoctorState extends State { children: [ Expanded( child: CustomButton( - text: "Later", + text: LocaleKeys.later.tr(context: context), backgroundColor: Color(0xffFEE9EA), borderColor: Color(0xffFEE9EA), textColor: Color(0xffED1C2B), @@ -184,12 +184,18 @@ class _RateAppointmentDoctorState extends State { SizedBox(width: 10), Expanded( child: CustomButton( - text: LocaleKeys.next.tr(context: context), - onPressed: () { + text: LocaleKeys.submit.tr(context: context), + onPressed: () async { // Set up clinic rating and show clinic rating view - appointmentRatingViewModel!.setTitle(LocaleKeys.rateDoctor.tr(context: context),); - appointmentRatingViewModel!.setSubTitle(LocaleKeys.howWasYourLastVisitWithDoctor.tr(context: context),); - appointmentRatingViewModel!.setClinicOrDoctor(true); + // appointmentRatingViewModel!.setTitle(LocaleKeys.rateDoctor.tr(context: context),); + // appointmentRatingViewModel!.setSubTitle(LocaleKeys.howWasYourLastVisitWithDoctor.tr(context: context),); + // appointmentRatingViewModel!.setClinicOrDoctor(true); + + LoaderBottomSheet.showLoader(); + await appointmentRatingViewModel!.submitDoctorRating(docRate: rating, docNote: note); + // await appointmentRatingViewModel!.submitClinicRating(clinicRate: rating, clinicNote: note); + LoaderBottomSheet.hideLoader(); + Navigator.pop(context); setState(() {}); }, diff --git a/lib/presentation/rate_appointment/widget/doctor_row.dart b/lib/presentation/rate_appointment/widget/doctor_row.dart index 725ac7c3..2a139d05 100644 --- a/lib/presentation/rate_appointment/widget/doctor_row.dart +++ b/lib/presentation/rate_appointment/widget/doctor_row.dart @@ -1,4 +1,3 @@ - import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/utils/date_util.dart'; @@ -8,6 +7,8 @@ import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/appointment_details_resp_model.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; +import 'dart:ui' as ui; + class BuildDoctorRow extends StatelessWidget { bool isForClinic = false; AppointmentDetails? appointmentDetails; @@ -36,32 +37,27 @@ class BuildDoctorRow extends StatelessWidget { SizedBox(height: 8.h), - isForClinic ? Wrap( direction: Axis.horizontal, spacing: 3.h, runSpacing: 4.h, children: [ AppCustomChipWidget( - labelText: appointmentDetails!.clinicName.toString(), - ), AppCustomChipWidget( icon: AppAssets.ic_date_filter, labelText: DateUtil.formatDateToDate(DateUtil.convertStringToDate(appointmentDetails!.appointmentDate), false), - + isEnglishOnly: true, ), AppCustomChipWidget( icon: AppAssets.appointment_time_icon, labelText: appointmentDetails!.startTime.substring(0, appointmentDetails!.startTime.length - 3), - ), - ] ) : Wrap( direction: Axis.horizontal, @@ -69,26 +65,30 @@ class BuildDoctorRow extends StatelessWidget { runSpacing: 4.h, children: [ AppCustomChipWidget( - labelText: appointmentDetails!.projectName.toString(), - - ), AppCustomChipWidget( - labelText: appointmentDetails!.clinicName.toString(), - - ) - - ] - ) - ], + ), + Directionality( + textDirection: ui.TextDirection.ltr, + child: AppCustomChipWidget( + labelPadding: EdgeInsetsDirectional.only(start: -8.w, end: 6.w), + icon: AppAssets.ic_date_filter, + labelText: DateUtil.formatDateToDate(DateUtil.convertStringToDate(appointmentDetails!.appointmentDate), false), + isEnglishOnly: true, + ), + ), + ], + ), + ], + ), ), - ), ], - )); + ), + ); } } \ No newline at end of file diff --git a/lib/presentation/smartwatches/activity_detail.dart b/lib/presentation/smartwatches/activity_detail.dart new file mode 100644 index 00000000..78527140 --- /dev/null +++ b/lib/presentation/smartwatches/activity_detail.dart @@ -0,0 +1,359 @@ +import 'dart:math'; + +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/common_models/data_points.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/smartwatch_health_data/health_provider.dart'; +import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; +import 'package:hmg_patient_app_new/widgets/custom_tab_bar.dart'; +import 'package:hmg_patient_app_new/widgets/graph/CustomBarGraph.dart'; +import 'package:intl/intl.dart' show DateFormat; +import 'package:provider/provider.dart'; +import 'package:hmg_patient_app_new/features/smartwatch_health_data/HealthDataTransformation.dart' as durations; +import 'package:dartz/dartz.dart' show Tuple2; + +import '../../core/utils/date_util.dart'; + +class ActivityDetails extends StatefulWidget { + final String selectedActivity; + final String sectionName; + final String uom; + + const ActivityDetails({super.key, required this.selectedActivity, required this.sectionName, required this.uom}); + + @override + State createState() => _ActivityDetailsState(); +} + +class _ActivityDetailsState extends State { + int index = 0; + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.bgScaffoldColor, + body: CollapsingListView( + title: "All Health Data".needTranslation, + child: Column( + spacing: 16.h, + children: [ + periodSelectorView((index) {}), + activityDetails(), + DecoratedBox( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 12.h), + child: + activityGraph().paddingOnly(left: 16.w, right: 16.w, top: 32.h, bottom: 16.h),) + ], + ).paddingSymmetrical(24.w, 24.h), + ), + ); + } + + Widget periodSelectorView(Function(int) onItemSelected) { + return DecoratedBox( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 12.h), + child: Row( + children: [ + Expanded( + child: CustomTabBar( + activeTextColor: Color(0xffED1C2B), + activeBackgroundColor: Color(0xffED1C2B).withValues(alpha: .1), + tabs: [ + CustomTabBarModel(null, "D"), + CustomTabBarModel(null, "W"), + CustomTabBarModel(null, "M"), + CustomTabBarModel(null, "6M"), + CustomTabBarModel(null, "Y"), + // CustomTabBarModel(null, "Completed".needTranslation), + ], + shouldTabExpanded: true, + onTabChange: (index) { + switch (index) { + case 0: + context.read().setDurations(durations.Durations.daily); + break; + case 1: + context.read().setDurations(durations.Durations.weekly); + break; + case 2: + context.read().setDurations(durations.Durations.monthly); + break; + case 3: + context.read().setDurations(durations.Durations.halfYearly); + break; + case 4: + context.read().setDurations(durations.Durations.yearly); + break; + } + context.read().fetchData(); + }, + ), + ), + ], + ).paddingSymmetrical(4.w, 4.h)); + } + + Widget activityDetails() { + return DecoratedBox( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 12.h, hasShadow: true), + child: Column( + spacing: 8.h, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + widget.sectionName.capitalizeFirstofEach.toText32(weight: FontWeight.w600, color: AppColors.textColor), + "Average".toText12(fontWeight: FontWeight.w500, color: AppColors.greyTextColor) + ], + ), + Selector>( + selector: (_, model) => Tuple2(model.averageValue, model.averageValueString), + builder: (_, data, __) { + var averageAsDouble = data.value1; + var averageAsString = data.value2; + return Row( + crossAxisAlignment: CrossAxisAlignment.baseline, + textBaseline: TextBaseline.alphabetic, + spacing: 4.w, + children: [ + (averageAsDouble?.toStringAsFixed(2) ?? averageAsString ?? "N/A").toText24(color: AppColors.textGreenColor, fontWeight: FontWeight.w600), + Visibility( + visible: averageAsDouble != null || averageAsString != null, + child: widget.uom.toText12(color: AppColors.greyTextColor, fontWeight: FontWeight.w500) + ) + ], + ); + }) + ], + ).paddingSymmetrical(16.w, 16.h)); + } + + + Widget activityGraph() { + // final _random = Random(); + // + // int randomBP() => 100 + _random.nextInt(51); // 100–150 + // final List data6Months = List.generate(6, (index) { + // final date = DateTime.now().subtract(Duration(days: 30 * (5 - index))); + // + // final value = randomBP(); + // + // return DataPoint( + // value: value.toDouble(), + // label: value.toString(), + // actualValue: value.toString(), + // displayTime: DateFormat('MMM').format(date), + // unitOfMeasurement: 'mmHg', + // time: date, + // ); + // }); + // final List data12Months = List.generate(12, (index) { + // final date = DateTime.now().subtract(Duration(days: 30 * (11 - index))); + // + // final value = randomBP(); + // + // return DataPoint( + // value: value.toDouble(), + // label: value.toString(), + // actualValue: value.toString(), + // displayTime: DateFormat('MMM').format(date), + // unitOfMeasurement: 'mmHg', + // time: date, + // ); + // }); + // + // List data =[]; + // if(index == 0){ + // data = data6Months; + // } else if(index == 1){ + // data = data12Months; + // } else + // data = [ + // DataPoint( + // value: 128, + // label: "128", + // actualValue: '128', + // displayTime: 'Sun', + // unitOfMeasurement: 'mmHg', + // time: DateTime.now().subtract(const Duration(days: 6)), + // ), + // DataPoint( + // value: 135, + // label: "135", + // actualValue: '135', + // displayTime: 'Mon', + // unitOfMeasurement: 'mmHg', + // time: DateTime.now().subtract(const Duration(days: 5)), + // ), + // DataPoint( + // value: 122, + // label: "122", + // actualValue: '122', + // displayTime: 'Tue', + // unitOfMeasurement: 'mmHg', + // time: DateTime.now().subtract(const Duration(days: 4)), + // ), + // DataPoint( + // value: 140, + // label: "140", + // actualValue: '140', + // displayTime: 'Wed', + // unitOfMeasurement: 'mmHg', + // time: DateTime.now().subtract(const Duration(days: 3)), + // ), + // DataPoint( + // value: 118, + // label: "118", + // actualValue: '118', + // displayTime: 'Thu', + // unitOfMeasurement: 'mmHg', + // time: DateTime.now().subtract(const Duration(days: 2)), + // ), + // DataPoint( + // value: 125, + // label: "125", + // actualValue: '125', + // displayTime: 'Fri', + // unitOfMeasurement: 'mmHg', + // time: DateTime.now().subtract(const Duration(days: 1)), + // ), + // DataPoint( + // value: 130, + // label: "130", + // actualValue: '130', + // displayTime: 'Sat', + // unitOfMeasurement: 'mmHg', + // time: DateTime.now(), + // ),23 + // ]; + return Selector>?>( + selector: (_, model) => model.selectedData, + builder: (_, data, __) { + if (context.read().selectedData.values.toList().first?.isEmpty == true) return SizedBox(); + + return CustomBarChart( + dataPoints: context.read().selectedData.values.toList().first, + height: 300.h, + maxY: 150, + barColor: AppColors.bgGreenColor, + barWidth: getBarWidth(), + barRadius: BorderRadius.circular(8), + bottomLabelColor: Colors.black, + bottomLabelSize: 12, + leftLabelInterval: .1, + leftLabelReservedSize: 20, + // Left axis label formatter (Y-axis) + leftLabelFormatter: (value) { + var labelValue = double.tryParse(value.toStringAsFixed(0)); + + if (labelValue == null) return SizedBox.shrink(); + // if (labelValue == 0 || labelValue == 150 / 2 || labelValue == 150) { + // return Text( + // labelValue.toStringAsFixed(0), + // style: const TextStyle( + // color: Colors.black26, + // fontSize: 11, + // ), + // ); + // } + + return SizedBox.shrink(); + }, + + /// for the handling of the sleep time + getTooltipItem: (widget.selectedActivity == "sleep") + ? (data) { + return BarTooltipItem( + '${DateUtil. millisToHourMin(num.parse(data.actualValue).toInt())}\n${DateFormat('dd MMM, yyyy').format(data.time)}', + TextStyle( + color: Colors.black, + fontSize: 12.f, + fontWeight: FontWeight.w500, + ), + ); + } + : null, + + // Bottom axis label formatter (X-axis - Days) + bottomLabelFormatter: (value, dataPoints) { + final index = value.toInt(); + print("value is $value"); + print("the index is $index"); + print("the dataPoints.length is ${dataPoints.length}"); + + var bottomText = ""; + var date = dataPoints[index].time; + print("the time is ${date}"); + switch (context.read().selectedDuration) { + case durations.Durations.daily: + bottomText = getHour(date).toString(); + break; + case durations.Durations.weekly: + bottomText = getDayName(date)[0]; + break; + case durations.Durations.monthly: + case durations.Durations.halfYearly: + case durations.Durations.yearly: + bottomText = getMonthName(date)[0]; + } + + return Text( + bottomText, + style: const TextStyle( + color: Colors.grey, + fontSize: 11, + ), + ); + return const Text(''); + }, + verticalInterval: 1 / context.read().selectedData.values.toList().first.length, + getDrawingVerticalLine: (value) { + return FlLine( + color: AppColors.greyColor, + strokeWidth: 1, + ); + }, + showGridLines: true); + }); + } + + //todo remove these from here + String getDayName(DateTime date) { + return DateUtil.getWeekDayAsOfLang(date.weekday); + } + + String getHour(DateTime date) { + return date.hour.toString().padLeft(2, '0').toString(); + } + + static String getMonthName(DateTime date) { + return DateUtil.getMonthDayAsOfLang(date.month); + } + + double getBarWidth() { + var duration = context.read().selectedDuration; + switch(duration){ + case durations.Durations.daily: + return 26.w; + case durations.Durations.weekly: + return 26.w; + case durations.Durations.monthly: + return 6.w; + case durations.Durations.halfYearly: + return 26.w; + case durations.Durations.yearly: + return 18.w; + } + } +} diff --git a/lib/presentation/smartwatches/smart_watch_activity.dart b/lib/presentation/smartwatches/smart_watch_activity.dart new file mode 100644 index 00000000..678fd715 --- /dev/null +++ b/lib/presentation/smartwatches/smart_watch_activity.dart @@ -0,0 +1,252 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/smartwatch_health_data/health_provider.dart'; +import 'package:hmg_patient_app_new/presentation/smartwatches/activity_detail.dart'; +import 'package:hmg_patient_app_new/services/navigation_service.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; +import 'package:provider/provider.dart'; +import 'package:hmg_patient_app_new/features/smartwatch_health_data/HealthDataTransformation.dart' as durations; + +import '../../core/utils/date_util.dart' show DateUtil; + +class SmartWatchActivity extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.bgScaffoldColor, + body: CollapsingListView( + title: "All Health Data".needTranslation, + child: Column( + spacing: 16.h, + children: [ + resultItem( + leadingIcon: AppAssets.watchActivity, + title: "Activity Calories".needTranslation, + description: "Activity rings give you a quick visual reference of how active you are each day. ".needTranslation, + trailingIcon: AppAssets.watchActivityTrailing, + result: context.read().sumOfNonEmptyData(context.read().vitals?.activity??[]), + unitsOfMeasure: "Kcal" + ).onPress((){ + // Map> getVitals() { + // return { + // "heartRate": heartRate , + // "sleep": sleep, + // "steps": step, + // "activity": activity, + // "bodyOxygen": bodyOxygen, + // "bodyTemperature": bodyTemperature, + // }; + // } + context.read().setDurations(durations.Durations.daily); + + context.read().deleteDataIfSectionIsDifferent("activity"); + context.read().saveSelectedSection("activity"); + context.read().fetchData(); + context.read().navigateToDetails("activity", sectionName:"Activity Calories", uom: "Kcal"); + + }), + resultItem( + leadingIcon: AppAssets.watchSteps, + title: "Steps".needTranslation, + description: "Step count is the number of steps you take throughout the day.".needTranslation, + trailingIcon: AppAssets.watchStepsTrailing, + result: context.read().sumOfNonEmptyData(context.read().vitals?.step??[]), + unitsOfMeasure: "Steps" + ).onPress((){ + // Map> getVitals() { + // return { + // "heartRate": heartRate , + // "sleep": sleep, + // "steps": step, + // "activity": activity, + // "bodyOxygen": bodyOxygen, + // "bodyTemperature": bodyTemperature, + // }; + // } + context.read().setDurations(durations.Durations.daily); + + context.read().deleteDataIfSectionIsDifferent("steps"); + context.read().saveSelectedSection("steps"); + context.read().fetchData(); + context.read().navigateToDetails("steps", sectionName: "Steps", uom: "Steps"); + + }), + resultItem( + leadingIcon: AppAssets.watchSteps, + title: "Distance Covered".needTranslation, + description: "Step count is the distance you take throughout the day.".needTranslation, + trailingIcon: AppAssets.watchStepsTrailing, + result: context.read().sumOfNonEmptyData(context.read().vitals?.distance??[]), + unitsOfMeasure: "Km" + ).onPress((){ + // Map> getVitals() { + // return { + // "heartRate": heartRate , + // "sleep": sleep, + // "steps": step, + // "activity": activity, + // "bodyOxygen": bodyOxygen, + // "bodyTemperature": bodyTemperature, + // }; + // } + context.read().setDurations(durations.Durations.daily); + + context.read().deleteDataIfSectionIsDifferent("distance"); + context.read().saveSelectedSection("distance"); + context.read().fetchData(); + context.read().navigateToDetails("distance", sectionName: "Distance Covered", uom: "km"); + + }), + + resultItem( + leadingIcon: AppAssets.watchSleep, + title: "Sleep Score".needTranslation, + description: "This will keep track of how much hours you sleep in a day".needTranslation, + trailingIcon: AppAssets.watchSleepTrailing, + result: DateUtil.millisToHourMin(int.parse(context.read().firstNonEmptyValue(context.read().vitals?.sleep??[]))).split(" ")[0], + unitsOfMeasure: "hr", + resultSecondValue: DateUtil.millisToHourMin(int.parse(context.read().firstNonEmptyValue(context.read().vitals?.sleep??[]))).split(" ")[2], + unitOfSecondMeasure: "min" + ).onPress((){ + // Map> getVitals() { + // return { + // "heartRate": heartRate , + // "sleep": sleep, + // "steps": step, + // "activity": activity, + // "bodyOxygen": bodyOxygen, + // "bodyTemperature": bodyTemperature, + // }; + // } + context.read().setDurations(durations.Durations.daily); + + context.read().deleteDataIfSectionIsDifferent("sleep"); + context.read().saveSelectedSection("sleep"); + context.read().fetchData(); + context.read().navigateToDetails("sleep", sectionName:"Sleep Score",uom:""); + + }), + + resultItem( + leadingIcon: AppAssets.watchWeight, + title: "Blood Oxygen".needTranslation, + description: "This will calculate your Blood Oxygen to keep track and update history".needTranslation, + trailingIcon: AppAssets.watchWeightTrailing, + result: context.read().firstNonEmptyValue(context.read().vitals?.bodyOxygen??[], ), + unitsOfMeasure: "%" + ).onPress((){ + // Map> getVitals() { + // return { + // "heartRate": heartRate , + // "sleep": sleep, + // "steps": step, + // "activity": activity, + // "bodyOxygen": bodyOxygen, + // "bodyTemperature": bodyTemperature, + // }; + // } + context.read().setDurations(durations.Durations.daily); + + context.read().deleteDataIfSectionIsDifferent("bodyOxygen"); + context.read().saveSelectedSection("bodyOxygen"); + context.read().fetchData(); + context.read().navigateToDetails("bodyOxygen", uom: "%", sectionName:"Blood Oxygen" ); + + }), + resultItem( + leadingIcon: AppAssets.watchWeight, + title: "Body temperature".needTranslation, + description: "This will calculate your Body temprerature to keep track and update history".needTranslation, + trailingIcon: AppAssets.watchWeightTrailing, + result: context.read().firstNonEmptyValue(context.read().vitals?.bodyTemperature??[]), + unitsOfMeasure: "C" + ).onPress((){ + // Map> getVitals() { + // return { + // "heartRate": heartRate , + // "sleep": sleep, + // "steps": step, + // "activity": activity, + // "bodyOxygen": bodyOxygen, + // "bodyTemperature": bodyTemperature, + // }; + // } + context.read().setDurations(durations.Durations.daily); + + context.read().deleteDataIfSectionIsDifferent("bodyTemperature"); + context.read().saveSelectedSection("bodyTemperature"); + context.read().fetchData(); + context.read().navigateToDetails("bodyTemperature" , sectionName: "Body temperature".capitalizeFirstofEach, uom: "C"); + + }), + ], + ).paddingSymmetrical(24.w, 24.h), + )); + } + + Widget resultItem({ + required String leadingIcon, + required String title, + required String description, + required String trailingIcon, + required String result, + required String unitsOfMeasure, + String? resultSecondValue, + String? unitOfSecondMeasure + }) { + return DecoratedBox( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 12.h), + child: Row( + spacing: 16.w, + children: [ + Expanded( + child:Column( + spacing: 8.h, + children: [ + Row( + spacing: 8.w, + children: [ + Utils.buildSvgWithAssets(icon: leadingIcon, height: 16.h, width: 14.w), + title.toText16( weight: FontWeight.w600, color: AppColors.textColor), + ], + ), + description.toText12(fontWeight: FontWeight.w500, color: AppColors.greyTextColor), + Row( + crossAxisAlignment: CrossAxisAlignment.baseline, + textBaseline: TextBaseline.alphabetic, + spacing: 2.h, + children: [ + result.toText21(weight: FontWeight.w600, color: AppColors.textColor), + unitsOfMeasure.toText10(weight: FontWeight.w500, color:AppColors.greyTextColor ), + if(resultSecondValue != null) + Visibility( + visible: resultSecondValue != null , + child: Row( + crossAxisAlignment: CrossAxisAlignment.baseline, + textBaseline: TextBaseline.alphabetic, + spacing: 2.h, + children: [ + SizedBox(width: 2.w,), + resultSecondValue.toText21(weight: FontWeight.w600, color: AppColors.textColor), + unitOfSecondMeasure!.toText10(weight: FontWeight.w500, color:AppColors.greyTextColor ) + ], + ), + ) + ], + ), + + ], + ) , + ), + Utils.buildSvgWithAssets(icon: trailingIcon, width: 72.w, height: 72.h), + ], + ).paddingSymmetrical(16.w, 16.h) + ); + } +} diff --git a/lib/presentation/smartwatches/smartwatch_home_page.dart b/lib/presentation/smartwatches/smartwatch_home_page.dart index 8fd79098..800ff030 100644 --- a/lib/presentation/smartwatches/smartwatch_home_page.dart +++ b/lib/presentation/smartwatches/smartwatch_home_page.dart @@ -3,18 +3,25 @@ import 'dart:io'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/common_models/smart_watch.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/extensions/route_extensions.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/features/smartwatch_health_data/health_provider.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/presentation/smartwatches/smartwatch_instructions_page.dart'; import 'package:hmg_patient_app_new/presentation/smartwatches/widgets/supported_watches_list.dart'; +import 'package:hmg_patient_app_new/services/navigation_service.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; import 'package:provider/provider.dart'; +import '../../core/utils/utils.dart'; + class SmartwatchHomePage extends StatelessWidget { const SmartwatchHomePage({super.key}); @@ -80,7 +87,15 @@ class SmartwatchHomePage extends StatelessWidget { CustomButton( text: LocaleKeys.select.tr(context: context), onPressed: () { - context.read().setSelectedWatchType("apple", "assets/images/png/smartwatches/apple-watch-5.jpg"); + context.read().setSelectedWatchType(SmartWatchTypes.apple, "assets/images/png/smartwatches/apple-watch-5.jpg"); + getIt.get().pushPage(page: SmartwatchInstructionsPage( + smartwatchDetails: SmartwatchDetails(SmartWatchTypes.apple, + "assets/images/png/smartwatches/apple-watch-5.jpg", + AppAssets.bluetooth, + LocaleKeys.applehealthapplicationshouldbeinstalledinyourphone.tr(context: context), + LocaleKeys.unabletodetectapplicationinstalledpleasecomebackonceinstalled.tr(context: context), + LocaleKeys.applewatchshouldbeconnected.tr(context: context)), + )); }, backgroundColor: AppColors.primaryRedColor.withAlpha(40), borderColor: AppColors.primaryRedColor.withAlpha(0), @@ -105,8 +120,15 @@ class SmartwatchHomePage extends StatelessWidget { CustomButton( text: LocaleKeys.select.tr(context: context), onPressed: () { - context.read().setSelectedWatchType("samsung", "assets/images/png/smartwatches/galaxy_watch_8_classic.jpeg"); - }, + context.read().setSelectedWatchType(SmartWatchTypes.samsung, "assets/images/png/smartwatches/galaxy_watch_8_classic.jpeg"); + getIt.get().pushPage(page: SmartwatchInstructionsPage( + smartwatchDetails: SmartwatchDetails(SmartWatchTypes.samsung, + "assets/images/png/smartwatches/galaxy_watch_8_classic.jpeg", + AppAssets.bluetooth, + LocaleKeys.samsunghealthapplicationshouldbeinstalledinyourphone.tr(context: context), + LocaleKeys.unabletodetectapplicationinstalledpleasecomebackonceinstalled.tr(context: context), + LocaleKeys.samsungwatchshouldbeconnected.tr(context: context)), + )); }, backgroundColor: AppColors.primaryRedColor.withAlpha(40), borderColor: AppColors.primaryRedColor.withAlpha(0), textColor: AppColors.primaryRedColor, @@ -130,7 +152,16 @@ class SmartwatchHomePage extends StatelessWidget { CustomButton( text: LocaleKeys.select.tr(context: context), onPressed: () { - context.read().setSelectedWatchType("huawei", "assets/images/png/smartwatches/Huawei_Watch.png"); + // context.read().setSelectedWatchType(SmartWatchTypes.huawei, "assets/images/png/smartwatches/Huawei_Watch.png"); + // getIt.get().pushPage(page: SmartwatchInstructionsPage( + // smartwatchDetails: SmartwatchDetails(SmartWatchTypes.huawei, + // "assets/images/png/smartwatches/Huawei_Watch.png", + // AppAssets.bluetooth, + // LocaleKeys.huaweihealthapplicationshouldbeinstalledinyourphone.tr(context: context), + // LocaleKeys.unabletodetectapplicationinstalledpleasecomebackonceinstalled.tr(context: context), + // LocaleKeys.huaweiwatchshouldbeconnected.tr(context: context)), + // )); + showUnavailableDialog(context); }, backgroundColor: AppColors.primaryRedColor.withAlpha(40), borderColor: AppColors.primaryRedColor.withAlpha(0), @@ -155,7 +186,17 @@ class SmartwatchHomePage extends StatelessWidget { CustomButton( text: LocaleKeys.select.tr(context: context), onPressed: () { - context.read().setSelectedWatchType("whoop", "assets/images/png/smartwatches/Whoop_Watch.png"); + + showUnavailableDialog(context); + // context.read().setSelectedWatchType(SmartWatchTypes.whoop, "assets/images/png/smartwatches/Whoop_Watch.png"); + // getIt.get().pushPage(page: SmartwatchInstructionsPage( + // smartwatchDetails: SmartwatchDetails(SmartWatchTypes.whoop, + // "assets/images/png/smartwatches/Whoop_Watch.png", + // AppAssets.bluetooth, + // LocaleKeys.whoophealthapplicationshouldbeinstalledinyourphone.tr(context: context), + // LocaleKeys.unabletodetectapplicationinstalledpleasecomebackonceinstalled.tr(context: context), + // LocaleKeys.whoopwatchshouldbeconnected.tr(context: context)), + // )); }, backgroundColor: AppColors.primaryRedColor.withAlpha(40), borderColor: AppColors.primaryRedColor.withAlpha(0), @@ -177,4 +218,23 @@ class SmartwatchHomePage extends StatelessWidget { ), ); } + + void showUnavailableDialog(BuildContext context) { + + showCommonBottomSheetWithoutHeight( + title: LocaleKeys.notice.tr(context: context), + context, + child: Utils.getWarningWidget( + loadingText: LocaleKeys.featureComingSoonDescription.tr(context: context), + isShowActionButtons: false, + showOkButton: true, + onConfirmTap: () async { + context.pop(); + } + ), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + } } diff --git a/lib/presentation/smartwatches/smartwatch_instructions_page.dart b/lib/presentation/smartwatches/smartwatch_instructions_page.dart index 48683d50..fa5f3135 100644 --- a/lib/presentation/smartwatches/smartwatch_instructions_page.dart +++ b/lib/presentation/smartwatches/smartwatch_instructions_page.dart @@ -1,15 +1,26 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_assets.dart'; +import 'package:hmg_patient_app_new/core/common_models/smart_watch.dart'; +import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/generated/locale_keys.g.dart'; +import 'package:hmg_patient_app_new/presentation/smartwatches/smart_watch_activity.dart' show SmartWatchActivity; +import 'package:hmg_patient_app_new/services/navigation_service.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; +import 'package:provider/provider.dart'; + +import '../../core/utils/utils.dart'; +import '../../features/smartwatch_health_data/health_provider.dart' show HealthProvider; class SmartwatchInstructionsPage extends StatelessWidget { - const SmartwatchInstructionsPage({super.key}); + final SmartwatchDetails smartwatchDetails; + + const SmartwatchInstructionsPage({super.key, required this.smartwatchDetails}); @override Widget build(BuildContext context) { @@ -25,6 +36,7 @@ class SmartwatchInstructionsPage extends StatelessWidget { child: CustomButton( text: LocaleKeys.getStarted.tr(context: context), onPressed: () { + context.read().initDevice(); }, backgroundColor: AppColors.primaryRedColor, borderColor: AppColors.primaryRedColor, @@ -35,8 +47,55 @@ class SmartwatchInstructionsPage extends StatelessWidget { height: 50.h, ).paddingSymmetrical(24.w, 30.h), ), - child: SingleChildScrollView(), + child: Column( + mainAxisSize: MainAxisSize.max, + spacing: 18.h, + children: [ + Image.asset(smartwatchDetails.watchIcon, fit: BoxFit.contain, height: 280.h,width: 280.w,), + DecoratedBox( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 12.h), + child: Column( + children: [ + watchContentDetails( + title: smartwatchDetails.detailsTitle, + description: smartwatchDetails.details, + icon: smartwatchDetails.smallIcon, + descriptionTextColor: AppColors.primaryRedColor + ), + Divider( + color: AppColors.dividerColor, + thickness: 1.h, + ).paddingOnly(top: 16.h, bottom: 16.h), + watchContentDetails( + title: smartwatchDetails.secondTitle, + description: LocaleKeys.updatetheinformation.tr(), + icon: AppAssets.bluetooth, + descriptionTextColor: AppColors.greyTextColor + ), + ], + ).paddingSymmetrical(16.w, 16.h), + ) + ], + ).paddingSymmetrical(24.w, 16.h), ), ); } + + + Widget watchContentDetails({required String title, required String description, required String icon, required Color descriptionTextColor}) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 8.h, + children: [ + DecoratedBox( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 12.h), + child: Utils.buildSvgWithAssets(icon: icon, width: 40.w, height: 40.h), + + ), + + title.toText16(weight: FontWeight.w600, color: AppColors.textColor), + description.toText12(fontWeight: FontWeight.w500, color: descriptionTextColor) + ], + ); + } } diff --git a/lib/presentation/symptoms_checker/possible_conditions_screen.dart b/lib/presentation/symptoms_checker/possible_conditions_screen.dart index 323a13c2..79e28a05 100644 --- a/lib/presentation/symptoms_checker/possible_conditions_screen.dart +++ b/lib/presentation/symptoms_checker/possible_conditions_screen.dart @@ -2,8 +2,10 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/app_assets.dart'; import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/dependencies.dart'; import 'package:hmg_patient_app_new/core/enums.dart'; +import 'package:hmg_patient_app_new/core/location_util.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; import 'package:hmg_patient_app_new/extensions/route_extensions.dart'; import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; @@ -35,6 +37,8 @@ class PossibleConditionsPage extends StatelessWidget { late BookAppointmentsViewModel bookAppointmentsViewModel; late AppointmentViaRegionViewmodel regionalViewModel; + late AppState appState; + Widget _buildLoadingShimmer() { return ListView.separated( shrinkWrap: true, @@ -221,6 +225,7 @@ class PossibleConditionsPage extends StatelessWidget { symptomsCheckerViewModel = context.read(); bookAppointmentsViewModel = context.read(); regionalViewModel = context.read(); + appState = getIt.get(); return Scaffold( backgroundColor: AppColors.bgScaffoldColor, body: CollapsingListView( @@ -246,7 +251,7 @@ class PossibleConditionsPage extends StatelessWidget { // Get selected symptoms names for display final symptoms = symptomsCheckerViewModel .getAllSelectedSymptoms() - .map((s) => s.commonName ?? s.name ?? '') + .map((s) => (appState.isArabic() ? s.commonNameAr ?? s.nameAr ?? '' : s.commonName ?? s.name ?? '')) .where((name) => name.isNotEmpty) .take(3) .toList(); @@ -337,6 +342,8 @@ class PossibleConditionsPage extends StatelessWidget { selectedFacility: data.selectedFacility, hmcCount: data.hmcCount, hmgCount: data.hmgCount, + sortByLocation: data.sortByLocation, + onSortByLocationToggle: (value) => _handleSortByLocationToggle(value, data), ); } if (data.bottomSheetState == AppointmentViaRegionState.DOCTOR_SELECTION) { @@ -354,4 +361,41 @@ class PossibleConditionsPage extends StatelessWidget { } return SizedBox.shrink(); } + + void _handleSortByLocationToggle(bool value, AppointmentViaRegionViewmodel regionVM) { + if (value) { + final locationUtils = getIt.get(); + locationUtils.getLocation( + isShowConfirmDialog: true, + onSuccess: (latLng) { + regionVM.setSortByLocation(true); + _refreshHospitalListAfterApi(regionVM); + }, + onFailure: () { + regionVM.setSortByLocation(false); + }, + onLocationDeniedForever: () { + regionVM.setSortByLocation(false); + }, + ); + } else { + appState.resetLocation(); + regionVM.setSortByLocation(false); + _refreshHospitalListAfterApi(regionVM); + } + } + + void _refreshHospitalListAfterApi(AppointmentViaRegionViewmodel regionVM) { + void listener() { + if (!bookAppointmentsViewModel.isRegionListLoading) { + bookAppointmentsViewModel.removeListener(listener); + final selectedRegion = regionVM.selectedRegionId; + if (selectedRegion != null && bookAppointmentsViewModel.hospitalList?.registeredDoctorMap?[selectedRegion] != null) { + regionVM.setDisplayListAndRegionHospitalList(bookAppointmentsViewModel.hospitalList!.registeredDoctorMap![selectedRegion]); + } + } + } + bookAppointmentsViewModel.addListener(listener); + bookAppointmentsViewModel.getRegionMappedProjectList(); + } } diff --git a/lib/presentation/symptoms_checker/user_info_selection.dart b/lib/presentation/symptoms_checker/user_info_selection.dart index b245c014..8d58e049 100644 --- a/lib/presentation/symptoms_checker/user_info_selection.dart +++ b/lib/presentation/symptoms_checker/user_info_selection.dart @@ -175,9 +175,13 @@ class _UserInfoSelectionPageState extends State { int? displayAge = viewModel.selectedAge ?? userAgeFromDOB; String ageText = displayAge != null ? "$displayAge ${LocaleKeys.years.tr(context: context)}" : LocaleKeys.notSet.tr(context: context); - String heightText = viewModel.selectedHeight != null ? "${viewModel.selectedHeight!.round()} ${viewModel.isHeightCm ? 'cm' : 'ft'}" : LocaleKeys.notSet.tr(context: context); + String heightText = (viewModel.selectedHeight != null && viewModel.selectedHeight != 0) + ? "${viewModel.selectedHeight!.round()} ${viewModel.isHeightCm ? 'cm' : 'ft'}" + : LocaleKeys.notSet.tr(context: context); - String weightText = viewModel.selectedWeight != null ? "${viewModel.selectedWeight!.round()} ${viewModel.isWeightKg ? 'kg' : 'lbs'}" : LocaleKeys.notSet.tr(context: context); + String weightText = (viewModel.selectedWeight != null && viewModel.selectedWeight != 0) + ? "${viewModel.selectedWeight!.round()} ${viewModel.isWeightKg ? 'kg' : 'lbs'}" + : LocaleKeys.notSet.tr(context: context); return Column( children: [ diff --git a/lib/presentation/symptoms_checker/user_info_selection/widgets/custom_date_picker.dart b/lib/presentation/symptoms_checker/user_info_selection/widgets/custom_date_picker.dart index 98051e04..d2b82ef5 100644 --- a/lib/presentation/symptoms_checker/user_info_selection/widgets/custom_date_picker.dart +++ b/lib/presentation/symptoms_checker/user_info_selection/widgets/custom_date_picker.dart @@ -149,6 +149,7 @@ class _ThreeColumnDatePickerState extends State { fontWeight: selected ? FontWeight.w600 : FontWeight.w500, color: selected ? AppColors.textColor : AppColors.greyTextColor.withValues(alpha: 0.9), height: 1.0, + fontFamily: "Poppins", letterSpacing: selected ? -0.02 * 30 : -0.02 * 18, ), ); diff --git a/lib/presentation/todo_section/ancillary_order_payment_page.dart b/lib/presentation/todo_section/ancillary_order_payment_page.dart index 9d75c769..a8de2708 100644 --- a/lib/presentation/todo_section/ancillary_order_payment_page.dart +++ b/lib/presentation/todo_section/ancillary_order_payment_page.dart @@ -35,7 +35,7 @@ class AncillaryOrderPaymentPage extends StatefulWidget { final int orderNo; final int projectID; final List selectedProcedures; - final double totalAmount; + final num totalAmount; const AncillaryOrderPaymentPage({ super.key, @@ -491,7 +491,7 @@ class _AncillaryOrderPaymentPageState extends State { child: Utils.getSuccessWidget(loadingText: "${LocaleKeys.paymentCompletedSuccessfully.tr(context: context)}, ${LocaleKeys.hereIsYourInvoiceNumber.tr(context: context)}$invoiceNo"), isCloseButtonVisible: true, isDismissible: true, - isFullScreen: false, callBackFunc: () { + isFullScreen: false, isAutoDismiss: true, callBackFunc: () { // Pop until we reach the LandingPage todoSectionViewModel.setIsAncillaryOrdersNeedReloading(true); Navigator.pushAndRemoveUntil( diff --git a/lib/presentation/todo_section/ancillary_procedures_details_page.dart b/lib/presentation/todo_section/ancillary_procedures_details_page.dart index 38d4f978..b65d26df 100644 --- a/lib/presentation/todo_section/ancillary_procedures_details_page.dart +++ b/lib/presentation/todo_section/ancillary_procedures_details_page.dart @@ -24,6 +24,8 @@ import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; import 'package:provider/provider.dart'; +import 'dart:ui' as ui; + class AncillaryOrderDetailsList extends StatefulWidget { final int appointmentNoVida; final int orderNo; @@ -114,12 +116,13 @@ class _AncillaryOrderDetailsListState extends State { } } - double _getTotalAmount() { - double total = 0.0; + num _getTotalAmount() { + num total = 0.0; for (var proc in selectedProcedures) { total += (proc.patientShareWithTax ?? 0); } return total; + // return 1234.50; } @override @@ -253,8 +256,9 @@ class _AncillaryOrderDetailsListState extends State { children: [ AppCustomChipWidget( // icon: AppAssets.file_icon, - labelText: "MRN: ${patientMRN ?? 'N/A'}", + labelText: "${LocaleKeys.fileno.tr(context: context)}: ${patientMRN ?? 'N/A'}", iconSize: 12.w, + isEnglishOnly: true, ), // National ID @@ -263,20 +267,23 @@ class _AncillaryOrderDetailsListState extends State { // icon: AppAssets.card_user, labelText: "ID: $nationalID", iconSize: 12.w, + isEnglishOnly: true, ), // Appointment Number if (orderData.appointmentNo != null) AppCustomChipWidget( // icon: AppAssets.calendar, - labelText: "Appt #: ${orderData.appointmentNo}", + labelText: "${LocaleKeys.appointment.tr(context: context)}: ${orderData.appointmentNo}", iconSize: 12.w, + isEnglishOnly: true, ), // Order Number if (orderData.ancillaryOrderProcDetailsList?.firstOrNull?.orderNo != null) AppCustomChipWidget( labelText: "Order #: ${orderData.ancillaryOrderProcDetailsList!.first.orderNo}", + isEnglishOnly: true, ), // Blood Group @@ -297,12 +304,14 @@ class _AncillaryOrderDetailsListState extends State { backgroundColor: AppColors.successColor.withValues(alpha: 0.15), iconSize: 12.w, labelPadding: EdgeInsetsDirectional.only(start: -6.w, end: 8.w), + isEnglishOnly: true, ), // Policy Number if (orderData.insurancePolicyNo != null && orderData.insurancePolicyNo!.isNotEmpty) AppCustomChipWidget( - labelText: "Policy: ${orderData.insurancePolicyNo}", + labelText: "${LocaleKeys.policyNumber.tr(context: context)}: ${orderData.insurancePolicyNo}", + isEnglishOnly: true, ), AppCustomChipWidget( @@ -317,8 +326,12 @@ class _AncillaryOrderDetailsListState extends State { labelText: "Clinic: ${orderData.clinicName!}", ), if (orderData.clinicName != null && orderData.clinicName!.isNotEmpty) - AppCustomChipWidget( - labelText: "Date: ${DateFormat('MMM dd, yyyy').format(orderData.appointmentDate!)}", + Directionality( + textDirection: ui.TextDirection.ltr, + child: AppCustomChipWidget( + labelText: "${LocaleKeys.date.tr(context: context)}: ${DateFormat('MMM dd yyyy').format(orderData.appointmentDate!)}", + isEnglishOnly: true, + ), ), ], ), @@ -557,7 +570,7 @@ class _AncillaryOrderDetailsListState extends State { Row( children: [ Utils.getPaymentAmountWithSymbol( - (procedure.patientShare ?? 0).toStringAsFixed(2).toText13(weight: FontWeight.w600), + (procedure.patientShare ?? 0).toStringAsFixed(2).toText14(weight: FontWeight.w600, isEnglishOnly: true), AppColors.textColorLight, 13, isSaudiCurrency: true, @@ -576,7 +589,7 @@ class _AncillaryOrderDetailsListState extends State { Row( children: [ Utils.getPaymentAmountWithSymbol( - (procedure.patientTaxAmount ?? 0).toStringAsFixed(2).toText13(weight: FontWeight.w600), + (procedure.patientTaxAmount ?? 0).toStringAsFixed(2).toText12(fontWeight: FontWeight.w600, isEnglishOnly: true), AppColors.textColorLight, 13, isSaudiCurrency: true, @@ -595,7 +608,7 @@ class _AncillaryOrderDetailsListState extends State { Row( children: [ Utils.getPaymentAmountWithSymbol( - (procedure.patientShareWithTax ?? 0).toStringAsFixed(2).toText13(weight: FontWeight.w600), + (procedure.patientShareWithTax ?? 0).toStringAsFixed(2).toText12(fontWeight: FontWeight.w600, isEnglishOnly: true), AppColors.textColorLight, 13, isSaudiCurrency: true, @@ -640,13 +653,13 @@ class _AncillaryOrderDetailsListState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ SizedBox( - width: 150.h, + width: 200.h, child: Utils.getPaymentMethods(), ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Utils.getPaymentAmountWithSymbol(_getTotalAmount().toStringAsFixed(2).toText24(isBold: true), AppColors.blackColor, 17, isSaudiCurrency: true), + Utils.getPaymentAmountWithSymbol(NumberFormat.decimalPattern().format(_getTotalAmount()).toText24(isBold: true, isEnglishOnly: true), AppColors.blackColor, 17, isSaudiCurrency: true), ], ), ], diff --git a/lib/presentation/todo_section/widgets/ancillary_orders_list.dart b/lib/presentation/todo_section/widgets/ancillary_orders_list.dart index 6a41252e..c05d7fc9 100644 --- a/lib/presentation/todo_section/widgets/ancillary_orders_list.dart +++ b/lib/presentation/todo_section/widgets/ancillary_orders_list.dart @@ -15,6 +15,8 @@ import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/chip/app_custom_chip_widget.dart'; import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart'; +import 'dart:ui' as ui; + class AncillaryOrdersList extends StatelessWidget { final List orders; final Function(AncillaryOrderItem order)? onCheckIn; @@ -173,20 +175,26 @@ class AncillaryOrderCard extends StatelessWidget { if (order.orderNo != null || isLoading) AppCustomChipWidget( // icon: AppAssets.calendar, - labelText: "${LocaleKeys.orderNumber.tr(context: context)}${order.orderNo ?? '-'}", - ).toShimmer2(isShow: isLoading), + labelText: "${LocaleKeys.orderNumber.tr(context: context)}${order.orderNo ?? '-'}", + isEnglishOnly: true) + .toShimmer2(isShow: isLoading), // Appointment Date if (order.appointmentDate != null || isLoading) - AppCustomChipWidget( - icon: AppAssets.appointment_calendar_icon, - labelText: isLoading ? "Date: Jan 20, 2024" : DateFormat('MMM dd, yyyy').format(order.appointmentDate!), - ).toShimmer2(isShow: isLoading), + Directionality( + textDirection: ui.TextDirection.ltr, + child: AppCustomChipWidget( + icon: AppAssets.appointment_calendar_icon, + labelText: isLoading ? "Date: Jan 20, 2024" : DateFormat('MMM dd, yyyy').format(order.appointmentDate!), + isEnglishOnly: true, + ).toShimmer2(isShow: isLoading), + ), // Appointment Number if (order.appointmentNo != null || isLoading) AppCustomChipWidget( - labelText: isLoading ? "Appt# : 98765" : "Appt #: ${order.appointmentNo}", + labelText: isLoading ? "Appt# : 98765" : "${LocaleKeys.appointment.tr(context: context)}: ${order.appointmentNo}", + isEnglishOnly: true, ).toShimmer2(isShow: isLoading), // Invoice Number diff --git a/lib/presentation/vital_sign/vital_sign_details_page.dart b/lib/presentation/vital_sign/vital_sign_details_page.dart index ca6e09a5..3d6f97be 100644 --- a/lib/presentation/vital_sign/vital_sign_details_page.dart +++ b/lib/presentation/vital_sign/vital_sign_details_page.dart @@ -16,6 +16,8 @@ import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/widgets/graph/custom_graph.dart'; import 'package:provider/provider.dart'; +import 'dart:ui' as ui; + /// Which vital sign is being shown in the details screen. enum VitalSignMetric { bmi, @@ -89,6 +91,21 @@ class _VitalSignDetailsPageState extends State { ), _whatIsThisResultCard(context), _historyCard(context, history: history), + Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 16.r, + hasShadow: false, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LocaleKeys.history.tr(context: context).toText16(isBold: true, color: AppColors.textColor), + SizedBox(height: 16.h), + _buildHistoryList(context, history), + ], + ).paddingSymmetrical(16.h, 16.h), + ), ], ).paddingAll(24.h), ); @@ -124,15 +141,13 @@ class _VitalSignDetailsPageState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ title.toText28(isBold: true, color: AppColors.textColor, letterSpacing: -1), - - ], ), SizedBox(height: 8.h), (latestDate != null ? LocaleKeys.resultOf.tr(namedArgs: {'date': latestDate.toString().split(' ').first}) : LocaleKeys.resultOfNoDate.tr(context: context)) - .toText11(weight: FontWeight.w500, color: AppColors.greyTextColor), + .toText11(weight: FontWeight.w500, color: AppColors.greyTextColor, isEnglishOnly: true), ], ), Row( @@ -264,35 +279,27 @@ class _VitalSignDetailsPageState extends State { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - _isGraphVisible ? LocaleKeys.historyFlowchart.tr(context: context) : LocaleKeys.history.tr(context: context), - style: TextStyle( - fontSize: 16, - fontFamily: 'Poppins', - fontWeight: FontWeight.w600, - color: AppColors.textColor, - ), - ), + LocaleKeys.historyFlowchart.tr(context: context).toText16(isBold: true, color: AppColors.textColor), Row( mainAxisSize: MainAxisSize.min, children: [ - Container( - width: 24.h, - height: 24.h, - alignment: Alignment.center, - child: InkWell( - onTap: () { - setState(() { - _isGraphVisible = !_isGraphVisible; - }); - }, - child: Utils.buildSvgWithAssets( - icon: _isGraphVisible ? AppAssets.ic_list : AppAssets.ic_graph, - width: 24.h, - height: 24.h, - ), - ), - ), + // Container( + // width: 24.h, + // height: 24.h, + // alignment: Alignment.center, + // child: InkWell( + // onTap: () { + // setState(() { + // _isGraphVisible = !_isGraphVisible; + // }); + // }, + // child: Utils.buildSvgWithAssets( + // icon: _isGraphVisible ? AppAssets.ic_list : AppAssets.ic_graph, + // width: 24.h, + // height: 24.h, + // ), + // ), + // ), // SizedBox(width: 16.h), // Container( // width: 24.h, @@ -423,10 +430,14 @@ class _VitalSignDetailsPageState extends State { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - dp.displayTime.toText12(color: AppColors.greyTextColor, fontWeight: FontWeight.w500), - valueText.toText12( - color: AppColors.textColor, - fontWeight: FontWeight.w600, + dp.displayTime.toText12(color: AppColors.greyTextColor, fontWeight: FontWeight.w500, isEnglishOnly: true), + Directionality( + textDirection: ui.TextDirection.ltr, + child: valueText.toText12( + color: AppColors.textColor, + fontWeight: FontWeight.w600, + isEnglishOnly: true + ), ), ], ), @@ -663,7 +674,8 @@ class _VitalSignDetailsPageState extends State { case VitalSignMetric.height: return null; case VitalSignMetric.weight: - return latest.weightKg != null ? 'Normal' : null; + // return latest.weightKg != null ? 'Normal' : null; + return VitalSignUiModel.bmiStatus(latest.bodyMassIndex); case VitalSignMetric.temperature: return null; case VitalSignMetric.heartRate: @@ -748,7 +760,7 @@ class _VitalSignDetailsPageState extends State { top: 8.0, right: isLast ? 16.h : 0, ), - child: label.toText8(fontWeight: FontWeight.w500), + child: label.toText10(weight: FontWeight.w500, isEnglishOnly: true), ); } diff --git a/lib/presentation/vital_sign/vital_sign_page.dart b/lib/presentation/vital_sign/vital_sign_page.dart index 200a223e..4fa90233 100644 --- a/lib/presentation/vital_sign/vital_sign_page.dart +++ b/lib/presentation/vital_sign/vital_sign_page.dart @@ -69,16 +69,16 @@ class _VitalSignPageState extends State { : null; // Debug logging for blood pressure - if (latestVitalSign != null) { - print('=== Blood Pressure Debug ==='); - print('bloodPressureHigher: ${latestVitalSign.bloodPressureHigher}'); - print('bloodPressureLower: ${latestVitalSign.bloodPressureLower}'); - print('bloodPressureHigher type: ${latestVitalSign.bloodPressureHigher.runtimeType}'); - print('bloodPressureLower type: ${latestVitalSign.bloodPressureLower.runtimeType}'); - print('bloodPressureHigher == 0: ${latestVitalSign.bloodPressureHigher == 0}'); - print('bloodPressureLower == 0: ${latestVitalSign.bloodPressureLower == 0}'); - print('========================'); - } + // if (latestVitalSign != null) { + // print('=== Blood Pressure Debug ==='); + // print('bloodPressureHigher: ${latestVitalSign.bloodPressureHigher}'); + // print('bloodPressureLower: ${latestVitalSign.bloodPressureLower}'); + // print('bloodPressureHigher type: ${latestVitalSign.bloodPressureHigher.runtimeType}'); + // print('bloodPressureLower type: ${latestVitalSign.bloodPressureLower.runtimeType}'); + // print('bloodPressureHigher == 0: ${latestVitalSign.bloodPressureHigher == 0}'); + // print('bloodPressureLower == 0: ${latestVitalSign.bloodPressureLower == 0}'); + // print('========================'); + // } return SingleChildScrollView( child: Column( @@ -138,11 +138,13 @@ class _VitalSignPageState extends State { label: LocaleKeys.weight.tr(context: context), value: latestVitalSign?.weightKg?.toString() ?? '--', unit: 'kg', - status: (latestVitalSign?.weightKg != null) ? 'Normal' : null, + status: (latestVitalSign?.weightKg != null) + ? VitalSignUiModel.bmiStatus(latestVitalSign?.bodyMassIndex) + : null, onTap: () => _openDetails( VitalSignDetailsArgs( metric: VitalSignMetric.weight, - title: LocaleKeys.height.tr(context: context), + title: LocaleKeys.weight.tr(context: context), icon: AppAssets.weightVital, unit: 'kg', ), diff --git a/lib/routes/app_routes.dart b/lib/routes/app_routes.dart index 01cc50b7..f22c6805 100644 --- a/lib/routes/app_routes.dart +++ b/lib/routes/app_routes.dart @@ -41,6 +41,7 @@ import '../features/monthly_reports/monthly_reports_repo.dart'; import '../features/monthly_reports/monthly_reports_view_model.dart'; import '../features/qr_parking/qr_parking_view_model.dart'; import '../presentation/parking/paking_page.dart'; +import '../presentation/smartwatches/smartwatch_instructions_page.dart'; import '../services/error_handler_service.dart'; class AppRoutes { diff --git a/lib/services/dialog_service.dart b/lib/services/dialog_service.dart index d2ba2723..ac7acf9e 100644 --- a/lib/services/dialog_service.dart +++ b/lib/services/dialog_service.dart @@ -23,7 +23,7 @@ abstract class DialogService { Future showExceptionBottomSheet({required String message, required Function() onOkPressed, Function()? onCancelPressed}); - Future showCommonBottomSheetWithoutH({String? label, required String message, String? okLabel, String? cancelLabel, required Function() onOkPressed, Function()? onCancelPressed}); + Future showCommonBottomSheetWithoutH({String? label, required String message, String? okLabel, String? cancelLabel, bool isConfirmButton = false, required Function() onOkPressed, Function()? onCancelPressed}); Future showSuccessBottomSheetWithoutH({String? label, required String message, required Function() onOkPressed, Function()? onCancelPressed}); @@ -119,7 +119,7 @@ class DialogServiceImp implements DialogService { } @override - Future showCommonBottomSheetWithoutH({String? label, required String message, String? okLabel, String? cancelLabel, required Function() onOkPressed, Function()? onCancelPressed}) async { + Future showCommonBottomSheetWithoutH({String? label, required String message, String? okLabel, String? cancelLabel, bool isConfirmButton = false, required Function() onOkPressed, Function()? onCancelPressed}) async { final context = navigationService.navigatorKey.currentContext; if (context == null) return; showCommonBottomSheetWithoutHeight( @@ -132,6 +132,7 @@ class DialogServiceImp implements DialogService { cancelLabel: cancelLabel, onOkPressed: onOkPressed, onCancelPressed: onCancelPressed, + isConfirmButton: isConfirmButton ), callBackFunc: () {}, ); @@ -239,7 +240,7 @@ class DialogServiceImp implements DialogService { } } -Widget exceptionBottomSheetWidget({required BuildContext context, required String message, String? okLabel, String? cancelLabel, required Function() onOkPressed, Function()? onCancelPressed}) { +Widget exceptionBottomSheetWidget({required BuildContext context, required String message, String? okLabel, String? cancelLabel, bool isConfirmButton = false, required Function() onOkPressed, Function()? onCancelPressed}) { return Column( children: [ (message).toText16(isBold: false, color: AppColors.textColor), @@ -267,7 +268,7 @@ Widget exceptionBottomSheetWidget({required BuildContext context, required Strin text: okLabel ?? LocaleKeys.confirm.tr(context: context), onPressed: onOkPressed, backgroundColor: AppColors.bgGreenColor, - borderColor: AppColors.bgGreenColor, + borderColor: AppColors.successColor, textColor: Colors.white, icon: AppAssets.confirm, ), @@ -284,8 +285,9 @@ Widget exceptionBottomSheetWidget({required BuildContext context, required Strin context.pop(); } : onCancelPressed, - backgroundColor: AppColors.primaryRedColor, - borderColor: AppColors.primaryRedBorderColor, + // backgroundColor: AppColors.primaryRedColor, + backgroundColor: isConfirmButton ? AppColors.successColor : AppColors.primaryRedColor, + borderColor: isConfirmButton ? AppColors.successColor : AppColors.primaryRedColor, textColor: Colors.white, icon: AppAssets.confirm, ), diff --git a/lib/services/livecare_permission_service.dart b/lib/services/livecare_permission_service.dart index a9a5d294..b8d71ff1 100644 --- a/lib/services/livecare_permission_service.dart +++ b/lib/services/livecare_permission_service.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:io'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; @@ -29,6 +30,7 @@ class LiveCarePermissionService { Permission.camera, Permission.microphone, Permission.notification, + if (Platform.isAndroid) Permission.systemAlertWindow, ]; try { @@ -87,6 +89,7 @@ class LiveCarePermissionService { if (permission == Permission.camera) return 'Camera'; if (permission == Permission.microphone) return 'Microphone'; if (permission == Permission.notification) return 'Notifications'; + if (permission == Permission.systemAlertWindow) return 'Display over other apps'; return permission.toString(); } } diff --git a/lib/splashPage.dart b/lib/splashPage.dart index 8f1ff4a6..64518f30 100644 --- a/lib/splashPage.dart +++ b/lib/splashPage.dart @@ -57,7 +57,7 @@ class _SplashScreenState extends State { await notificationService.initialize(onNotificationClick: (payload) { // Handle notification click here }); - //ZoomService().initializeZoomSDK(); + ZoomService().initializeZoomSDK(); if (isAppOpenedFromCall) { navigateToTeleConsult(); } else { diff --git a/lib/theme/colors.dart b/lib/theme/colors.dart index 467edd92..e9cd83eb 100644 --- a/lib/theme/colors.dart +++ b/lib/theme/colors.dart @@ -8,7 +8,7 @@ class AppColors { static const transparent = Colors.transparent; // ── Scaffold / Background ───────────────────────────────────────────────── - static Color get scaffoldBgColor => isDarkMode ? dark.scaffoldBgColor : const Color(0xFFF0F0F0); + static Color get scaffoldBgColor => isDarkMode ? dark.scaffoldBgColor : const Color(0xFFF8F8F8); static Color get bottomSheetBgColor => isDarkMode ? dark.bottomSheetBgColor : const Color(0xFFF8F8FA); static Color get lightGreyEFColor => isDarkMode ? dark.lightGreyEFColor : const Color(0xffeaeaff); static Color get greyF7Color => isDarkMode ? dark.greyF7Color : const Color(0xffF7F7F7); @@ -41,6 +41,17 @@ class AppColors { static Color get ratingColorYellow => isDarkMode ? dark.ratingColorYellow : const Color(0xFFFFAF15); static Color get spacerLineColor => isDarkMode ? dark.spacerLineColor : const Color(0x2E30391A); + // ── Doctor Rating Colors ────────────────────────────────────────────────── + static const Color ratingStarIconColor = Color(0xFFFFA726); // Orange-amber for star icons + static const Color ratingFiveStarColor = Color(0xFF18C273); // Green for 5 stars + static const Color ratingFourStarColor = Color(0xFF8BC34A); // Light green for 4 stars + static const Color ratingThreeStarColor = Color(0xFFC0A000); // Dark yellow/olive for 3 stars + static const Color ratingTwoStarColor = Color(0xFFD84315); // Dark orange-red for 2 stars + static const Color ratingOneStarColor = Color(0xFFD32F2F); // Red for 1 star + static const Color ratingBadgeFiveStarColor = Color(0xFF5BBD8E); // Green badge for ≥4.0 + static const Color ratingPercentageTextColor = Color(0xFF4A4A4A); // Dark grey for percentage text + + // ── Semantic / Status Colors ────────────────────────────────────────────── static Color get errorColor => isDarkMode ? dark.errorColor : const Color(0xFFED1C2B); static Color get alertColor => isDarkMode ? dark.alertColor : const Color(0xFFD48D05); @@ -300,6 +311,7 @@ extension AppColorsContext on BuildContext { // Shimmer Color get shimmerBaseColor => _isDark ? AppColors.dark.shimmerBaseColor : const Color(0xFFE0E0E0); Color get shimmerHighlightColor => _isDark ? AppColors.dark.shimmerHighlightColor : const Color(0xFFF5F5F5); + static const Color tooltipColor = Color(0xFF1AACACAC); // Aliases Color get bgScaffoldColor => scaffoldBgColor; diff --git a/lib/widgets/app_language_change.dart b/lib/widgets/app_language_change.dart index 1200c53b..4a589ae7 100644 --- a/lib/widgets/app_language_change.dart +++ b/lib/widgets/app_language_change.dart @@ -57,9 +57,9 @@ class _AppLanguageChangeState extends State { decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.h, hasShadow: true), child: Column( children: [ - languageItem("English", "en"), - 1.divider, languageItem("العربية", "ar"), + 1.divider, + languageItem("English", "en"), ], ), ), diff --git a/lib/widgets/common_bottom_sheet.dart b/lib/widgets/common_bottom_sheet.dart index bc52c0cc..9b5fa025 100644 --- a/lib/widgets/common_bottom_sheet.dart +++ b/lib/widgets/common_bottom_sheet.dart @@ -116,6 +116,7 @@ void showCommonBottomSheet(BuildContext context, Function(String?)? callBackFunc, String? title, required double height, + bool isAutoDismiss = false, bool isCloseButtonVisible = true, bool isFullScreen = true, bool isDismissible = true, @@ -131,6 +132,14 @@ void showCommonBottomSheet(BuildContext context, isDismissible: isDismissible, backgroundColor: isSuccessDialog ? AppColors.whiteColor : AppColors.scaffoldBgColor, builder: (BuildContext context) { + if (isAutoDismiss) { + Future.delayed(Duration(seconds: 2), () { + Navigator.of(context).pop(); + if (callBackFunc != null) { + callBackFunc(null); + } + }); + } return Container( height: height, decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.scaffoldBgColor, borderRadius: 24.h), @@ -211,9 +220,11 @@ void showCommonBottomSheetWithoutHeight( required Widget child, VoidCallback? callBackFunc, String title = "", + bool isConfirmButton = false, bool isCloseButtonVisible = true, bool isFullScreen = true, bool isDismissible = true, + bool isAutoDismiss = false, Widget? titleWidget, bool useSafeArea = false, bool hasBottomPadding = true, @@ -237,6 +248,14 @@ void showCommonBottomSheetWithoutHeight( backgroundColor: resolvedBgColor, useSafeArea: useSafeArea, builder: (BuildContext context) { + if (isAutoDismiss) { + Future.delayed(Duration(seconds: 2), () { + Navigator.of(context).pop(); + if (callBackFunc != null) { + callBackFunc(); + } + }); + } return SafeArea( top: false, left: false, diff --git a/lib/widgets/custom_tab_bar.dart b/lib/widgets/custom_tab_bar.dart index 0fd354e0..79741add 100644 --- a/lib/widgets/custom_tab_bar.dart +++ b/lib/widgets/custom_tab_bar.dart @@ -21,6 +21,7 @@ class CustomTabBar extends StatefulWidget { final Color? inActiveTextColor; final Color? inActiveBackgroundColor; final Function(int)? onTabChange; + final bool shouldTabExpanded; const CustomTabBar({ super.key, @@ -31,6 +32,7 @@ class CustomTabBar extends StatefulWidget { this.activeBackgroundColor, this.inActiveBackgroundColor, this.onTabChange, + this.shouldTabExpanded = false }); @override @@ -46,6 +48,14 @@ class CustomTabBarState extends State { super.initState(); } + @override + void didUpdateWidget(covariant CustomTabBar oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.initialIndex != widget.initialIndex) { + selectedIndex = widget.initialIndex; + } + } + @override void dispose() { super.dispose(); @@ -62,6 +72,11 @@ class CustomTabBarState extends State { final resolvedActiveBgColor = widget.activeBackgroundColor ?? AppColors.lightGrayBGColor; final resolvedInActiveBgColor = widget.inActiveBackgroundColor ?? AppColors.whiteColor; late Widget parentWidget; + if(widget.shouldTabExpanded){ + return Row( + children:List.generate(widget.tabs.length, (index)=>myTab(widget.tabs[index], index, resolvedActiveTextColor, resolvedInActiveTextColor, resolvedActiveBgColor, resolvedInActiveBgColor).expanded), + ); + } if (widget.tabs.length > 3) { parentWidget = ListView.separated( diff --git a/lib/widgets/graph/CustomBarGraph.dart b/lib/widgets/graph/CustomBarGraph.dart new file mode 100644 index 00000000..2f50f20d --- /dev/null +++ b/lib/widgets/graph/CustomBarGraph.dart @@ -0,0 +1,250 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/common_models/data_points.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; + +/// A customizable bar chart widget using `fl_chart`. +/// +/// Displays a bar chart with configurable axis labels, colors, and data points. +/// Useful for visualizing comparative data, categories, or grouped values. +/// +/// **Parameters:** +/// - [dataPoints]: List of `DataPoint` objects to plot. +/// - [secondaryDataPoints]: Optional list for grouped bars (e.g., comparison data). +/// - [leftLabelFormatter]: Function to build left axis labels. +/// - [bottomLabelFormatter]: Function to build bottom axis labels. +/// - [width]: Optional width of the chart. +/// - [height]: Required height of the chart. +/// - [maxY], [maxX], [minX]: Axis bounds. +/// - [barColor]: Color of the bars. +/// - [secondaryBarColor]: Color of the secondary bars. +/// - [barRadius]: Border radius for bar corners. +/// - [barWidth]: Width of each bar. +/// - [bottomLabelColor]: Color of bottom axis labels. +/// - [bottomLabelSize]: Font size for bottom axis labels. +/// - [bottomLabelFontWeight]: Font weight for bottom axis labels. +/// - [leftLabelInterval]: Interval between left axis labels. +/// - [leftLabelReservedSize]: Reserved space for left axis labels. +/// - [showBottomTitleDates]: Whether to show bottom axis labels. +/// - [isFullScreeGraph]: Whether the graph is fullscreen. +/// - [makeGraphBasedOnActualValue]: Use `actualValue` for plotting. +/// +/// Example usage: +/// ```dart +/// CustomBarChart( +/// dataPoints: sampleData, +/// leftLabelFormatter: (value) => ..., +/// bottomLabelFormatter: (value, dataPoints) => ..., +/// height: 300, +/// maxY: 100, +/// ) +class CustomBarChart extends StatelessWidget { + final List dataPoints; + final List? secondaryDataPoints; // For grouped bar charts + final double? width; + final double height; + final double? maxY; + final double? maxX; + final double? minX; + Color? barColor; + final Color? secondaryBarColor; + Color? barGridColor; + Color? bottomLabelColor; + final double? bottomLabelSize; + final FontWeight? bottomLabelFontWeight; + final double? leftLabelInterval; + final double? leftLabelReservedSize; + final double? bottomLabelReservedSize; + final bool? showGridLines; + final GetDrawingGridLine? getDrawingVerticalLine; + final double? verticalInterval; + final double? minY; + final BorderRadius? barRadius; + final double barWidth; + final BarTooltipItem Function(DataPoint)? getTooltipItem; + + /// Creates the left label and provides it to the chart + final Widget Function(double) leftLabelFormatter; + final Widget Function(double, List) bottomLabelFormatter; + + final bool showBottomTitleDates; + final bool isFullScreeGraph; + final bool makeGraphBasedOnActualValue; + + CustomBarChart( + {super.key, + required this.dataPoints, + this.secondaryDataPoints, + required this.leftLabelFormatter, + this.width, + required this.height, + this.maxY, + this.maxX, + this.showBottomTitleDates = true, + this.isFullScreeGraph = false, + this.secondaryBarColor, + this.bottomLabelFontWeight = FontWeight.w500, + this.bottomLabelSize, + this.leftLabelInterval, + this.leftLabelReservedSize, + this.bottomLabelReservedSize, + this.makeGraphBasedOnActualValue = false, + required this.bottomLabelFormatter, + this.minX, + this.showGridLines = false, + this.getDrawingVerticalLine, + this.verticalInterval, + this.minY, + this.barRadius, + this.barWidth = 16, + this.getTooltipItem, + this.barColor , + this.barGridColor , + this.bottomLabelColor, + }); + + @override + Widget build(BuildContext context) { + barColor ??= AppColors.bgGreenColor; + barGridColor ??= AppColors.graphGridColor; + bottomLabelColor ??= AppColors.textColor; + return Material( + color: Colors.white, + child: SizedBox( + width: width, + height: height, + child: BarChart( + BarChartData( + minY: minY ?? 0, + maxY: maxY, + + barTouchData: BarTouchData( + handleBuiltInTouches: true, + touchCallback: (FlTouchEvent event, BarTouchResponse? touchResponse) { + // Let fl_chart handle the touch + }, + + touchTooltipData: BarTouchTooltipData( + getTooltipColor: (_)=>AppColorsContext.tooltipColor, + getTooltipItem: (group, groupIndex, rod, rodIndex) { + final dataPoint = dataPoints[groupIndex]; + if(getTooltipItem != null) { + return getTooltipItem!(dataPoint); + } + + return BarTooltipItem( + '${dataPoint.actualValue} ${dataPoint.unitOfMeasurement ?? ""}\n${DateFormat('dd MMM, yyyy').format(dataPoint.time)}', + TextStyle( + color: Colors.black, + fontSize: 12.f, + fontWeight: FontWeight.w500, + ), + ); + }, + ), + enabled: true, + ), + titlesData: FlTitlesData( + leftTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: leftLabelReservedSize ?? 80, + interval: leftLabelInterval ?? .1, + getTitlesWidget: (value, _) { + return leftLabelFormatter(value); + }, + ), + ), + bottomTitles: AxisTitles( + axisNameSize: 20, + sideTitles: SideTitles( + showTitles: showBottomTitleDates, + reservedSize: bottomLabelReservedSize ?? 30, + getTitlesWidget: (value, _) { + return bottomLabelFormatter(value, dataPoints); + }, + interval: 1, + ), + ), + topTitles: AxisTitles(), + rightTitles: AxisTitles(), + ), + borderData: FlBorderData( + show: true, + border: const Border( + bottom: BorderSide.none, + left: BorderSide(color: Colors.grey, width: .5), + right: BorderSide.none, + top: BorderSide.none, + ), + ), + barGroups: _buildBarGroups(dataPoints), + + gridData: FlGridData( + show: showGridLines ?? true, + drawHorizontalLine: false, + verticalInterval: verticalInterval, + getDrawingVerticalLine: getDrawingVerticalLine ?? + (value) { + return FlLine( + color: barGridColor, + strokeWidth: 1, + dashArray: [5, 5], + ); + }, + )), + ), + ), + ); + } + + /// Builds bar chart groups from data points + List _buildBarGroups(List dataPoints) { + return dataPoints.asMap().entries.map((entry) { + final index = entry.key; + final dataPoint = entry.value; + double value = (makeGraphBasedOnActualValue) + ? double.tryParse(dataPoint.actualValue) ?? 0.0 + : dataPoint.value; + + final barRods = [ + BarChartRodData( + toY: value, + color: barColor, + width: barWidth, + borderRadius: barRadius ?? BorderRadius.circular(6), + // backDrawRodData: BackgroundBarChartRodData( + // show: true, + // toY: maxY, + // color: Colors.grey[100], + // ), + ), + ]; + + // Add secondary bar if provided (for grouped bar charts) + if (secondaryDataPoints != null && index < secondaryDataPoints!.length) { + final secondaryDataPoint = secondaryDataPoints![index]; + double secondaryValue = (makeGraphBasedOnActualValue) + ? double.tryParse(secondaryDataPoint.actualValue) ?? 0.0 + : secondaryDataPoint.value; + + barRods.add( + BarChartRodData( + toY: secondaryValue, + color: secondaryBarColor ?? AppColors.blueColor, + width: barWidth, + borderRadius: barRadius ?? BorderRadius.circular(6), + ), + ); + } + + return BarChartGroupData( + x: index, + barRods: barRods, + barsSpace: 8.w + ); + }).toList(); + } +} \ No newline at end of file diff --git a/lib/widgets/graph/custom_graph.dart b/lib/widgets/graph/custom_graph.dart index d9a69a99..898dc5ea 100644 --- a/lib/widgets/graph/custom_graph.dart +++ b/lib/widgets/graph/custom_graph.dart @@ -31,6 +31,7 @@ import 'package:hmg_patient_app_new/theme/colors.dart'; /// - [showBottomTitleDates]: Whether to show bottom axis labels. /// - [isFullScreeGraph]: Whether the graph is fullscreen. /// - [makeGraphBasedOnActualValue]: Use `actualValue` for plotting. +/// - [isRTL]: Whether the graph should render in right-to-left direction. /// /// Example usage: /// ```dart @@ -70,6 +71,7 @@ class CustomGraph extends StatelessWidget { final bool showLinePoints; final double? cutOffY; final RangeAnnotations? rangeAnnotations; + final bool isRTL; ///creates the left label and provide it to the chart as it will be used by other part of the application so the label will be different for every chart final Widget Function(double) leftLabelFormatter; @@ -113,7 +115,8 @@ class CustomGraph extends StatelessWidget { this.showShadow = false, this.showLinePoints = false, this.cutOffY = 0, - this.rangeAnnotations}); + this.rangeAnnotations, + this.isRTL = false}); @override Widget build(BuildContext context) { @@ -122,6 +125,12 @@ class CustomGraph extends StatelessWidget { final resolvedGraphShadowColor = graphShadowColor ?? AppColors.graphGridColor; final resolvedGraphGridColor = graphGridColor ?? AppColors.graphGridColor; + // For RTL, reverse the data points so the graph reads right-to-left + final displayDataPoints = isRTL ? dataPoints.reversed.toList() : dataPoints; + final displaySecondaryDataPoints = (isRTL && secondaryDataPoints != null) + ? secondaryDataPoints!.reversed.toList() + : secondaryDataPoints; + return Material( color: AppColors.whiteColor, child: SizedBox( @@ -176,14 +185,14 @@ class CustomGraph extends StatelessWidget { // Show tooltip for each touched line return touchedSpots.map((spot) { // Determine which dataset this spot belongs to - final isSecondary = secondaryDataPoints != null && spot.barIndex == 1; + final isSecondary = displaySecondaryDataPoints != null && spot.barIndex == 1; final dataPoint = isSecondary - ? secondaryDataPoints![spot.x.toInt()] - : dataPoints[spot.x.toInt()]; + ? displaySecondaryDataPoints[spot.x.toInt()] + : displayDataPoints[spot.x.toInt()]; return LineTooltipItem( '${dataPoint.actualValue} ${dataPoint.unitOfMeasurement ?? ""} - ${dataPoint.displayTime}', - TextStyle(color: Colors.black, fontSize: 12.f, fontWeight: FontWeight.w500), + TextStyle(color: Colors.black, fontSize: 12.f, fontWeight: FontWeight.w500, fontFamily: "Poppins"), ); }).toList(); }, @@ -192,9 +201,19 @@ class CustomGraph extends StatelessWidget { titlesData: FlTitlesData( leftTitles: AxisTitles( sideTitles: SideTitles( - showTitles: true, + showTitles: !isRTL, + reservedSize: leftLabelReservedSize ?? 80, + interval: leftLabelInterval ?? .1, + getTitlesWidget: (value, _) { + return leftLabelFormatter(value); + }, + ), + ), + rightTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: isRTL, reservedSize: leftLabelReservedSize ?? 80, - interval: leftLabelInterval ?? .1, // Let fl_chart handle it + interval: leftLabelInterval ?? .1, getTitlesWidget: (value, _) { return leftLabelFormatter(value); }, @@ -206,30 +225,28 @@ class CustomGraph extends StatelessWidget { showTitles: showBottomTitleDates, reservedSize: bottomLabelReservedSize ?? 20, getTitlesWidget: (value, _) { - return bottomLabelFormatter(value, dataPoints); + return bottomLabelFormatter(value, displayDataPoints); }, interval: 1, // ensures 1:1 mapping with spots ), ), topTitles: AxisTitles(), - rightTitles: AxisTitles(), ), borderData: FlBorderData( show: true, - border: const Border( + border: Border( bottom: BorderSide.none, - left: BorderSide(color: Colors.grey, width: .5), - right: BorderSide.none, + left: isRTL ? BorderSide.none : const BorderSide(color: Colors.grey, width: .5), + right: isRTL ? const BorderSide(color: Colors.grey, width: .5) : BorderSide.none, top: BorderSide.none, ), ), - lineBarsData: _buildColoredLineSegments(dataPoints, showLinePoints, resolvedGraphColor, resolvedGraphShadowColor), + lineBarsData: _buildColoredLineSegments(displayDataPoints, showLinePoints, resolvedGraphColor, resolvedGraphShadowColor, + secondaryPoints: displaySecondaryDataPoints), gridData: FlGridData( show: showGridLines ?? true, drawVerticalLine: false, horizontalInterval: horizontalInterval, - // checkToShowHorizontalLine: (value) => - // value >= 0 && value <= 100, getDrawingHorizontalLine: getDrawingHorizontalLine ?? (value) { return FlLine( @@ -245,7 +262,8 @@ class CustomGraph extends StatelessWidget { ); } - List _buildColoredLineSegments(List dataPoints, bool showLinePoints, Color resolvedGraphColor, Color resolvedGraphShadowColor) { + List _buildColoredLineSegments(List dataPoints, bool showLinePoints, Color resolvedGraphColor, Color resolvedGraphShadowColor, + {List? secondaryPoints}) { final List allSpots = dataPoints.asMap().entries.map((entry) { double value = (makeGraphBasedOnActualValue) ? double.tryParse(entry.value.actualValue) ?? 0.0 : entry.value.value; return FlSpot(entry.key.toDouble(), value); @@ -257,7 +275,7 @@ class CustomGraph extends StatelessWidget { isCurved: true, isStrokeCapRound: true, isStrokeJoinRound: true, - barWidth: 2, + barWidth: 3, gradient: LinearGradient( colors: [resolvedGraphColor, resolvedGraphColor], begin: Alignment.centerLeft, @@ -281,8 +299,8 @@ class CustomGraph extends StatelessWidget { ]; // Add secondary line if provided (for dual-line graphs like blood pressure) - if (secondaryDataPoints != null && secondaryDataPoints!.isNotEmpty) { - final List secondarySpots = secondaryDataPoints!.asMap().entries.map((entry) { + if (secondaryPoints != null && secondaryPoints.isNotEmpty) { + final List secondarySpots = secondaryPoints.asMap().entries.map((entry) { double value = (makeGraphBasedOnActualValue) ? double.tryParse(entry.value.actualValue) ?? 0.0 : entry.value.value; return FlSpot(entry.key.toDouble(), value); }).toList(); diff --git a/lib/widgets/input_widget.dart b/lib/widgets/input_widget.dart index 25dd4580..a64775aa 100644 --- a/lib/widgets/input_widget.dart +++ b/lib/widgets/input_widget.dart @@ -159,8 +159,8 @@ class TextInputWidget extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildLabelText(labelColor).paddingOnly( - right: (appState.getLanguageCode() == "ar" ? 10 : 0), - left: (appState.getLanguageCode() == "en" ? 10 : 0), + right: (appState.isArabic() ? 10 : 10), + left: (!appState.isArabic() ? 10 : 10), ), Row( children: [ @@ -230,7 +230,8 @@ class TextInputWidget extends StatelessWidget { language: appState.getLanguageCode()!, initialDate: DateTime.now(), showCalendarToggle: isHideSwitcher == true ? false : true, - fontFamily: appState.getLanguageCode() == "ar" ? "GESSTwo" : "Poppins", + // fontFamily: appState.getLanguageCode() == "ar" ? "GESSTwo" : "Poppins", + fontFamily: "Poppins", okWidget: Padding(padding: EdgeInsets.only(right: 8.h), child: Utils.buildSvgWithAssets(icon: AppAssets.confirm, width: 24.h, height: 24.h)), cancelWidget: Padding(padding: EdgeInsets.only(right: 8.h), child: Utils.buildSvgWithAssets(icon: AppAssets.cancel, iconColor: Colors.white, width: 24.h, height: 24.h)), onCalendarTypeChanged: (bool value) { @@ -289,15 +290,7 @@ class TextInputWidget extends StatelessWidget { } Widget _buildLabelText(Color? labelColor) { - return Text( - labelText, - style: TextStyle( - fontSize: 12.f, - fontWeight: FontWeight.w500, - color: labelColor ?? AppColors.inputLabelTextColor, - letterSpacing: -0, - ), - ); + return labelText.toText12(fontWeight: FontWeight.w500, color: labelColor ?? AppColors.inputLabelTextColor); } Widget _buildTextField(BuildContext context) { diff --git a/lib/widgets/loader/bottomsheet_loader.dart b/lib/widgets/loader/bottomsheet_loader.dart index bff293d0..306d7ce4 100644 --- a/lib/widgets/loader/bottomsheet_loader.dart +++ b/lib/widgets/loader/bottomsheet_loader.dart @@ -5,6 +5,8 @@ import 'package:hmg_patient_app_new/core/api_consts.dart'; import 'package:hmg_patient_app_new/core/enums.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/extensions/route_extensions.dart'; +import 'package:hmg_patient_app_new/extensions/string_extensions.dart'; import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; import 'package:hmg_patient_app_new/services/navigation_service.dart'; import 'package:hmg_patient_app_new/theme/colors.dart'; @@ -13,7 +15,7 @@ class LoaderBottomSheet { static final NavigationService _navService = GetIt.I(); static bool _isVisible = false; - static void showLoader({String? loadingText}) { + static void showLoader({String? loadingText, bool showCloseButton = false, Function? onCloseTap}) { if (_isVisible) return; _isVisible = true; @@ -32,7 +34,31 @@ class LoaderBottomSheet { color: AppColors.whiteColor, borderRadius: BorderRadius.vertical(top: Radius.circular(16)), ), - child: Center( + child: (showCloseButton) ? Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + "".toText20(), + if (showCloseButton) + IconButton( + icon: Icon(Icons.close, color: AppColors.textColor), + onPressed: () { + context.pop(); + if(onCloseTap != null) { + onCloseTap(); + } + }, + ) + else + SizedBox(width: 48), // Placeholder for alignment + ], + ), + Center( + child: Utils.getLoadingWidget(loadingText: loadingText), + ).paddingSymmetrical(24.w, 0), + ], + ) : Center( child: Utils.getLoadingWidget(loadingText: loadingText), ).paddingSymmetrical(24.w, 0), ); diff --git a/lib/widgets/routes/custom_page_route.dart b/lib/widgets/routes/custom_page_route.dart index b89d7102..7cddf18d 100644 --- a/lib/widgets/routes/custom_page_route.dart +++ b/lib/widgets/routes/custom_page_route.dart @@ -10,7 +10,6 @@ class CustomPageRoute extends PageRouteBuilder { CustomPageRoute({required this.page, this.direction = AxisDirection.right, this.fullScreenDialog = false, super.settings}) : super( - transitionDuration: const Duration(milliseconds: 1500), reverseTransitionDuration: const Duration(milliseconds: 500), fullscreenDialog: fullScreenDialog, diff --git a/lib/widgets/scroll_wheel_time_picker.dart b/lib/widgets/scroll_wheel_time_picker.dart new file mode 100644 index 00000000..e1a35d6f --- /dev/null +++ b/lib/widgets/scroll_wheel_time_picker.dart @@ -0,0 +1,184 @@ +import 'package:flutter/material.dart'; +import 'package:hmg_patient_app_new/core/app_export.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; + + +class ScrollWheelTimePicker extends StatefulWidget { + final int initialHour; + + final int initialMinute; + + final bool use24HourFormat; + + final ValueChanged? onTimeChanged; + + final double itemExtent; + + final double pickerHeight; + + final TextStyle? digitTextStyle; + + final TextStyle? separatorTextStyle; + + const ScrollWheelTimePicker({ + super.key, + this.initialHour = 8, + this.initialMinute = 15, + this.use24HourFormat = true, + this.onTimeChanged, + this.itemExtent = 60, + this.pickerHeight = 180, + this.digitTextStyle, + this.separatorTextStyle, + }); + + @override + State createState() => _ScrollWheelTimePickerState(); +} + +class _ScrollWheelTimePickerState extends State { + late FixedExtentScrollController _hourController; + late FixedExtentScrollController _minuteController; + late int _selectedHour; + late int _selectedMinute; + + int get _maxHour => widget.use24HourFormat ? 24 : 12; + + @override + void initState() { + super.initState(); + _selectedHour = widget.initialHour; + _selectedMinute = widget.initialMinute; + + final hourIndex = + widget.use24HourFormat ? _selectedHour : (_selectedHour == 0 ? 11 : _selectedHour - 1); + + _hourController = FixedExtentScrollController(initialItem: hourIndex); + _minuteController = FixedExtentScrollController(initialItem: _selectedMinute); + } + + @override + void dispose() { + _hourController.dispose(); + _minuteController.dispose(); + super.dispose(); + } + + void _notifyChange() { + widget.onTimeChanged?.call(TimeOfDay(hour: _selectedHour, minute: _selectedMinute)); + } + + TextStyle get _defaultDigitStyle => TextStyle( + fontSize: 72.f, + fontWeight: FontWeight.w800, + color: AppColors.textColor, + letterSpacing: 0, + ); + + TextStyle get _defaultSeparatorStyle => TextStyle( + fontSize: 40.f, + fontWeight: FontWeight.w800, + color: AppColors.textColor, + letterSpacing: 0, + ); + + @override + Widget build(BuildContext context) { + final digitStyle = widget.digitTextStyle ?? _defaultDigitStyle; + final separatorStyle = widget.separatorTextStyle ?? _defaultSeparatorStyle; + + return SizedBox( + height: widget.pickerHeight, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + width: 150.w, + child: ListWheelScrollView.useDelegate( + controller: _hourController, + itemExtent: widget.itemExtent, + physics: const FixedExtentScrollPhysics(), + perspective: 0.005, + diameterRatio: 1.2, + onSelectedItemChanged: (index) { + setState(() { + _selectedHour = + widget.use24HourFormat ? index : index + 1; + }); + _notifyChange(); + }, + childDelegate: ListWheelChildBuilderDelegate( + childCount: _maxHour, + builder: (context, index) { + final hour = + widget.use24HourFormat ? index : index + 1; + final isSelected = hour == _selectedHour; + return Visibility( + visible: true, + child: Center( + child: AnimatedDefaultTextStyle( + duration: const Duration(milliseconds: 200), + style: digitStyle.copyWith( + color: isSelected + ? AppColors.textColor + : AppColors.textColor.withValues(alpha: 0.3), + ), + child: Text( + hour.toString().padLeft(2, '0'), + ), + ), + ), + ); + }, + ), + ), + ), + + Padding( + padding: EdgeInsets.symmetric(horizontal: 2.w), + child: Text(':', style: separatorStyle), + ), + + SizedBox( + width: 150.w, + child: ListWheelScrollView.useDelegate( + controller: _minuteController, + itemExtent: widget.itemExtent, + physics: const FixedExtentScrollPhysics(), + perspective: 0.005, + diameterRatio: 1.2, + onSelectedItemChanged: (index) { + setState(() { + _selectedMinute = index; + }); + _notifyChange(); + }, + childDelegate: ListWheelChildBuilderDelegate( + childCount: 60, + + builder: (context, index) { + final isSelected = index == _selectedMinute; + return Visibility( + visible: true, + child: + Center( + child: AnimatedDefaultTextStyle( + duration: const Duration(milliseconds: 200), + style: digitStyle.copyWith( + color: isSelected + ? AppColors.textColor + : AppColors.transparent.withValues(alpha: 0.3), + ), + child: Text(index.toString().padLeft(2, '0')), + ), + )); + }, + ), + ), + ), + ], + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock new file mode 100644 index 00000000..8395a120 --- /dev/null +++ b/pubspec.lock @@ -0,0 +1,2097 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _flutterfire_internals: + dependency: transitive + description: + name: _flutterfire_internals + sha256: ff0a84a2734d9e1089f8aedd5c0af0061b82fb94e95260d943404e0ef2134b11 + url: "https://pub.dev" + source: hosted + version: "1.3.59" + adaptive_number: + dependency: transitive + description: + name: adaptive_number + sha256: "3a567544e9b5c9c803006f51140ad544aedc79604fd4f3f2c1380003f97c1d77" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + amazon_payfort: + dependency: "direct main" + description: + name: amazon_payfort + sha256: "7732df0764aecbb814f910db36d0dca2f696e7e5ea380b49aa3ec62965768b33" + url: "https://pub.dev" + source: hosted + version: "1.1.4" + archive: + dependency: transitive + description: + name: archive + sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd" + url: "https://pub.dev" + source: hosted + version: "4.0.7" + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + async: + dependency: transitive + description: + name: async + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + url: "https://pub.dev" + source: hosted + version: "2.13.0" + audio_session: + dependency: transitive + description: + name: audio_session + sha256: "8f96a7fecbb718cb093070f868b4cdcb8a9b1053dce342ff8ab2fde10eb9afb7" + url: "https://pub.dev" + source: hosted + version: "0.2.2" + auto_size_text: + dependency: "direct main" + description: + name: auto_size_text + sha256: "3f5261cd3fb5f2a9ab4e2fc3fba84fd9fcaac8821f20a1d4e71f557521b22599" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + barcode_scan2: + dependency: "direct main" + description: + name: barcode_scan2 + sha256: "9b539b0ce419005c451de66374c79f39801986f1fd7a213e63d948f21487cd69" + url: "https://pub.dev" + source: hosted + version: "4.7.2" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + cached_network_image: + dependency: "direct main" + description: + name: cached_network_image + sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916" + url: "https://pub.dev" + source: hosted + version: "3.4.1" + cached_network_image_platform_interface: + dependency: transitive + description: + name: cached_network_image_platform_interface + sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829" + url: "https://pub.dev" + source: hosted + version: "4.1.1" + cached_network_image_web: + dependency: transitive + description: + name: cached_network_image_web + sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062" + url: "https://pub.dev" + source: hosted + version: "1.3.1" + carp_serializable: + dependency: transitive + description: + name: carp_serializable + sha256: f039f8ea22e9437aef13fe7e9743c3761c76d401288dcb702eadd273c3e4dcef + url: "https://pub.dev" + source: hosted + version: "2.0.1" + characters: + dependency: transitive + description: + name: characters + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + chewie: + dependency: transitive + description: + name: chewie + sha256: "44bcfc5f0dfd1de290c87c9d86a61308b3282a70b63435d5557cfd60f54a69ca" + url: "https://pub.dev" + source: hosted + version: "1.13.0" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + connectivity_plus: + dependency: "direct main" + description: + name: connectivity_plus + sha256: b5e72753cf63becce2c61fd04dfe0f1c430cc5278b53a1342dc5ad839eab29ec + url: "https://pub.dev" + source: hosted + version: "6.1.5" + connectivity_plus_platform_interface: + dependency: transitive + description: + name: connectivity_plus_platform_interface + sha256: "42657c1715d48b167930d5f34d00222ac100475f73d10162ddf43e714932f204" + url: "https://pub.dev" + source: hosted + version: "2.0.1" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "701dcfc06da0882883a2657c445103380e53e647060ad8d9dfb710c100996608" + url: "https://pub.dev" + source: hosted + version: "0.3.5+1" + crypto: + dependency: transitive + description: + name: crypto + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + url: "https://pub.dev" + source: hosted + version: "3.0.7" + csslib: + dependency: transitive + description: + name: csslib + sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e" + url: "https://pub.dev" + source: hosted + version: "1.0.2" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 + url: "https://pub.dev" + source: hosted + version: "1.0.8" + dart_jsonwebtoken: + dependency: "direct main" + description: + name: dart_jsonwebtoken + sha256: "0de65691c1d736e9459f22f654ddd6fd8368a271d4e41aa07e53e6301eff5075" + url: "https://pub.dev" + source: hosted + version: "3.3.1" + dartz: + dependency: "direct main" + description: + name: dartz + sha256: e6acf34ad2e31b1eb00948692468c30ab48ac8250e0f0df661e29f12dd252168 + url: "https://pub.dev" + source: hosted + version: "0.10.1" + dbus: + dependency: transitive + description: + name: dbus + sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c" + url: "https://pub.dev" + source: hosted + version: "0.7.11" + device_calendar: + dependency: "direct main" + description: + path: "." + ref: HEAD + resolved-ref: "5ea5ed9e2bb499c0633383b53103f2920b634755" + url: "https://github.com/bardram/device_calendar" + source: git + version: "4.3.1" + device_calendar_plus: + dependency: "direct main" + description: + name: device_calendar_plus + sha256: d11a70d98eb123e8eb09fdcfaf220ca4f1aa65a1512e12092f176f4b54983507 + url: "https://pub.dev" + source: hosted + version: "0.3.3" + device_calendar_plus_android: + dependency: transitive + description: + name: device_calendar_plus_android + sha256: a341ef29fa0251251287d63c1d009dfd35c1459dc6a129fd5e03f5ac92d8d7ff + url: "https://pub.dev" + source: hosted + version: "0.3.3" + device_calendar_plus_ios: + dependency: transitive + description: + name: device_calendar_plus_ios + sha256: "3b2f84ce1ed002be8460e214a3229e66748bbaad4077603f2c734d67c42033ff" + url: "https://pub.dev" + source: hosted + version: "0.3.3" + device_calendar_plus_platform_interface: + dependency: transitive + description: + name: device_calendar_plus_platform_interface + sha256: "0ce7511c094ca256831a48e16efe8f1e97e7bd00a5ff3936296ffd650a1d76b5" + url: "https://pub.dev" + source: hosted + version: "0.3.3" + device_info_plus: + dependency: "direct main" + description: + name: device_info_plus + sha256: "98f28b42168cc509abc92f88518882fd58061ea372d7999aecc424345c7bff6a" + url: "https://pub.dev" + source: hosted + version: "11.5.0" + device_info_plus_platform_interface: + dependency: transitive + description: + name: device_info_plus_platform_interface + sha256: e1ea89119e34903dca74b883d0dd78eb762814f97fb6c76f35e9ff74d261a18f + url: "https://pub.dev" + source: hosted + version: "7.0.3" + dropdown_search: + dependency: "direct main" + description: + name: dropdown_search + sha256: c29b3e5147a82a06a4a08b3b574c51cb48cc17ad89893d53ee72a6f86643622e + url: "https://pub.dev" + source: hosted + version: "6.0.2" + easy_localization: + dependency: "direct main" + description: + name: easy_localization + sha256: "2ccdf9db8fe4d9c5a75c122e6275674508fd0f0d49c827354967b8afcc56bbed" + url: "https://pub.dev" + source: hosted + version: "3.0.8" + easy_logger: + dependency: transitive + description: + name: easy_logger + sha256: c764a6e024846f33405a2342caf91c62e357c24b02c04dbc712ef232bf30ffb7 + url: "https://pub.dev" + source: hosted + version: "0.0.2" + ed25519_edwards: + dependency: transitive + description: + name: ed25519_edwards + sha256: "6ce0112d131327ec6d42beede1e5dfd526069b18ad45dcf654f15074ad9276cd" + url: "https://pub.dev" + source: hosted + version: "0.3.1" + equatable: + dependency: "direct main" + description: + name: equatable + sha256: "3e0141505477fd8ad55d6eb4e7776d3fe8430be8e497ccb1521370c3f21a3e2b" + url: "https://pub.dev" + source: hosted + version: "2.0.8" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + ffi: + dependency: transitive + description: + name: ffi + sha256: d07d37192dbf97461359c1518788f203b0c9102cfd2c35a716b823741219542c + url: "https://pub.dev" + source: hosted + version: "2.1.5" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + file_picker: + dependency: "direct main" + description: + name: file_picker + sha256: d974b6ba2606371ac71dd94254beefb6fa81185bde0b59bdc1df09885da85fde + url: "https://pub.dev" + source: hosted + version: "10.3.8" + file_selector_linux: + dependency: transitive + description: + name: file_selector_linux + sha256: "2567f398e06ac72dcf2e98a0c95df2a9edd03c2c2e0cacd4780f20cdf56263a0" + url: "https://pub.dev" + source: hosted + version: "0.9.4" + file_selector_macos: + dependency: transitive + description: + name: file_selector_macos + sha256: "5e0bbe9c312416f1787a68259ea1505b52f258c587f12920422671807c4d618a" + url: "https://pub.dev" + source: hosted + version: "0.9.5" + file_selector_platform_interface: + dependency: transitive + description: + name: file_selector_platform_interface + sha256: "35e0bd61ebcdb91a3505813b055b09b79dfdc7d0aee9c09a7ba59ae4bb13dc85" + url: "https://pub.dev" + source: hosted + version: "2.7.0" + file_selector_windows: + dependency: transitive + description: + name: file_selector_windows + sha256: "62197474ae75893a62df75939c777763d39c2bc5f73ce5b88497208bc269abfd" + url: "https://pub.dev" + source: hosted + version: "0.9.3+5" + firebase_analytics: + dependency: "direct main" + description: + name: firebase_analytics + sha256: "4f85b161772e1d54a66893ef131c0a44bd9e552efa78b33d5f4f60d2caa5c8a3" + url: "https://pub.dev" + source: hosted + version: "11.6.0" + firebase_analytics_platform_interface: + dependency: transitive + description: + name: firebase_analytics_platform_interface + sha256: a44b6d1155ed5cae7641e3de7163111cfd9f6f6c954ca916dc6a3bdfa86bf845 + url: "https://pub.dev" + source: hosted + version: "4.4.3" + firebase_analytics_web: + dependency: transitive + description: + name: firebase_analytics_web + sha256: c7d1ed1f86ae64215757518af5576ff88341c8ce5741988c05cc3b2e07b0b273 + url: "https://pub.dev" + source: hosted + version: "0.5.10+16" + firebase_core: + dependency: "direct main" + description: + name: firebase_core + sha256: "7be63a3f841fc9663342f7f3a011a42aef6a61066943c90b1c434d79d5c995c5" + url: "https://pub.dev" + source: hosted + version: "3.15.2" + firebase_core_platform_interface: + dependency: transitive + description: + name: firebase_core_platform_interface + sha256: cccb4f572325dc14904c02fcc7db6323ad62ba02536833dddb5c02cac7341c64 + url: "https://pub.dev" + source: hosted + version: "6.0.2" + firebase_core_web: + dependency: transitive + description: + name: firebase_core_web + sha256: "0ed0dc292e8f9ac50992e2394e9d336a0275b6ae400d64163fdf0a8a8b556c37" + url: "https://pub.dev" + source: hosted + version: "2.24.1" + firebase_crashlytics: + dependency: "direct main" + description: + name: firebase_crashlytics + sha256: "662ae6443da91bca1fb0be8aeeac026fa2975e8b7ddfca36e4d90ebafa35dde1" + url: "https://pub.dev" + source: hosted + version: "4.3.10" + firebase_crashlytics_platform_interface: + dependency: transitive + description: + name: firebase_crashlytics_platform_interface + sha256: "7222a8a40077c79f6b8b3f3439241c9f2b34e9ddfde8381ffc512f7b2e61f7eb" + url: "https://pub.dev" + source: hosted + version: "3.8.10" + firebase_messaging: + dependency: "direct main" + description: + name: firebase_messaging + sha256: "60be38574f8b5658e2f22b7e311ff2064bea835c248424a383783464e8e02fcc" + url: "https://pub.dev" + source: hosted + version: "15.2.10" + firebase_messaging_platform_interface: + dependency: transitive + description: + name: firebase_messaging_platform_interface + sha256: "685e1771b3d1f9c8502771ccc9f91485b376ffe16d553533f335b9183ea99754" + url: "https://pub.dev" + source: hosted + version: "4.6.10" + firebase_messaging_web: + dependency: transitive + description: + name: firebase_messaging_web + sha256: "0d1be17bc89ed3ff5001789c92df678b2e963a51b6fa2bdb467532cc9dbed390" + url: "https://pub.dev" + source: hosted + version: "3.10.10" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" + fl_chart: + dependency: "direct main" + description: + name: fl_chart + sha256: "577aeac8ca414c25333334d7c4bb246775234c0e44b38b10a82b559dd4d764e7" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_cache_manager: + dependency: transitive + description: + name: flutter_cache_manager + sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386" + url: "https://pub.dev" + source: hosted + version: "3.4.1" + flutter_callkit_incoming: + dependency: "direct main" + description: + name: flutter_callkit_incoming + sha256: "3589deb8b71e43f2d520a9c8a5240243f611062a8b246cdca4b1fda01fbbf9b8" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + flutter_hooks: + dependency: transitive + description: + name: flutter_hooks + sha256: cde36b12f7188c85286fba9b38cc5a902e7279f36dd676967106c041dc9dde70 + url: "https://pub.dev" + source: hosted + version: "0.20.5" + flutter_inappwebview: + dependency: "direct main" + description: + name: flutter_inappwebview + sha256: "80092d13d3e29b6227e25b67973c67c7210bd5e35c4b747ca908e31eb71a46d5" + url: "https://pub.dev" + source: hosted + version: "6.1.5" + flutter_inappwebview_android: + dependency: transitive + description: + name: flutter_inappwebview_android + sha256: "62557c15a5c2db5d195cb3892aab74fcaec266d7b86d59a6f0027abd672cddba" + url: "https://pub.dev" + source: hosted + version: "1.1.3" + flutter_inappwebview_internal_annotations: + dependency: transitive + description: + name: flutter_inappwebview_internal_annotations + sha256: "787171d43f8af67864740b6f04166c13190aa74a1468a1f1f1e9ee5b90c359cd" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + flutter_inappwebview_ios: + dependency: transitive + description: + name: flutter_inappwebview_ios + sha256: "5818cf9b26cf0cbb0f62ff50772217d41ea8d3d9cc00279c45f8aabaa1b4025d" + url: "https://pub.dev" + source: hosted + version: "1.1.2" + flutter_inappwebview_macos: + dependency: transitive + description: + name: flutter_inappwebview_macos + sha256: c1fbb86af1a3738e3541364d7d1866315ffb0468a1a77e34198c9be571287da1 + url: "https://pub.dev" + source: hosted + version: "1.1.2" + flutter_inappwebview_platform_interface: + dependency: transitive + description: + name: flutter_inappwebview_platform_interface + sha256: cf5323e194096b6ede7a1ca808c3e0a078e4b33cc3f6338977d75b4024ba2500 + url: "https://pub.dev" + source: hosted + version: "1.3.0+1" + flutter_inappwebview_web: + dependency: transitive + description: + name: flutter_inappwebview_web + sha256: "55f89c83b0a0d3b7893306b3bb545ba4770a4df018204917148ebb42dc14a598" + url: "https://pub.dev" + source: hosted + version: "1.1.2" + flutter_inappwebview_windows: + dependency: transitive + description: + name: flutter_inappwebview_windows + sha256: "8b4d3a46078a2cdc636c4a3d10d10f2a16882f6be607962dbfff8874d1642055" + url: "https://pub.dev" + source: hosted + version: "0.6.0" + flutter_ios_voip_kit_karmm: + dependency: "direct main" + description: + name: flutter_ios_voip_kit_karmm + sha256: "31a445d78aacacdf128a0354efb9f4e424285dfe4c0af3ea872e64f03e6f6bfc" + url: "https://pub.dev" + source: hosted + version: "0.8.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" + url: "https://pub.dev" + source: hosted + version: "5.0.0" + flutter_local_notifications: + dependency: "direct main" + description: + name: flutter_local_notifications + sha256: "19ffb0a8bb7407875555e5e98d7343a633bb73707bae6c6a5f37c90014077875" + url: "https://pub.dev" + source: hosted + version: "19.5.0" + flutter_local_notifications_linux: + dependency: transitive + description: + name: flutter_local_notifications_linux + sha256: e3c277b2daab8e36ac5a6820536668d07e83851aeeb79c446e525a70710770a5 + url: "https://pub.dev" + source: hosted + version: "6.0.0" + flutter_local_notifications_platform_interface: + dependency: transitive + description: + name: flutter_local_notifications_platform_interface + sha256: "277d25d960c15674ce78ca97f57d0bae2ee401c844b6ac80fcd972a9c99d09fe" + url: "https://pub.dev" + source: hosted + version: "9.1.0" + flutter_local_notifications_windows: + dependency: transitive + description: + name: flutter_local_notifications_windows + sha256: "8d658f0d367c48bd420e7cf2d26655e2d1130147bca1eea917e576ca76668aaf" + url: "https://pub.dev" + source: hosted + version: "1.0.3" + flutter_localizations: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_nfc_kit: + dependency: "direct main" + description: + name: flutter_nfc_kit + sha256: "3cf589592373f1d0b0bd9583532368bb85e7cd76ae014a2b67a5ab2d68ae9450" + url: "https://pub.dev" + source: hosted + version: "3.6.1" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: ee8068e0e1cd16c4a82714119918efdeed33b3ba7772c54b5d094ab53f9b7fd1 + url: "https://pub.dev" + source: hosted + version: "2.0.33" + flutter_rating_bar: + dependency: "direct main" + description: + name: flutter_rating_bar + sha256: d2af03469eac832c591a1eba47c91ecc871fe5708e69967073c043b2d775ed93 + url: "https://pub.dev" + source: hosted + version: "4.0.1" + flutter_staggered_animations: + dependency: "direct main" + description: + name: flutter_staggered_animations + sha256: "81d3c816c9bb0dca9e8a5d5454610e21ffb068aedb2bde49d2f8d04f75538351" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + flutter_svg: + dependency: "direct main" + description: + name: flutter_svg + sha256: "87fbd7c534435b6c5d9d98b01e1fd527812b82e68ddd8bd35fc45ed0fa8f0a95" + url: "https://pub.dev" + source: hosted + version: "2.2.3" + flutter_swiper_view: + dependency: "direct main" + description: + name: flutter_swiper_view + sha256: "2a165b259e8a4c49d4da5626b967ed42a73dac2d075bd9e266ad8d23b9f01879" + url: "https://pub.dev" + source: hosted + version: "1.1.8" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + flutter_widget_from_html: + dependency: "direct main" + description: + name: flutter_widget_from_html + sha256: "7f1daefcd3009c43c7e7fb37501e6bb752d79aa7bfad0085fb0444da14e89bd0" + url: "https://pub.dev" + source: hosted + version: "0.17.1" + flutter_widget_from_html_core: + dependency: transitive + description: + name: flutter_widget_from_html_core + sha256: "1120ee6ed3509ceff2d55aa6c6cbc7b6b1291434422de2411b5a59364dd6ff03" + url: "https://pub.dev" + source: hosted + version: "0.17.0" + flutter_zoom_videosdk: + dependency: "direct main" + description: + name: flutter_zoom_videosdk + sha256: "46a4dea664b1c969099328a499c198a1755adf9ac333dea28bea5187910b3bf9" + url: "https://pub.dev" + source: hosted + version: "2.1.10" + fluttertoast: + dependency: "direct main" + description: + name: fluttertoast + sha256: "90778fe0497fe3a09166e8cf2e0867310ff434b794526589e77ec03cf08ba8e8" + url: "https://pub.dev" + source: hosted + version: "8.2.14" + fwfh_cached_network_image: + dependency: transitive + description: + name: fwfh_cached_network_image + sha256: "484cb5f8047f02cfac0654fca5832bfa91bb715fd7fc651c04eb7454187c4af8" + url: "https://pub.dev" + source: hosted + version: "0.16.1" + fwfh_chewie: + dependency: transitive + description: + name: fwfh_chewie + sha256: ae74fc26798b0e74f3983f7b851e74c63b9eeb2d3015ecd4b829096b2c3f8818 + url: "https://pub.dev" + source: hosted + version: "0.16.1" + fwfh_just_audio: + dependency: transitive + description: + name: fwfh_just_audio + sha256: dfd622a0dfe049ac647423a2a8afa7f057d9b2b93d92710b624e3d370b1ac69a + url: "https://pub.dev" + source: hosted + version: "0.17.0" + fwfh_svg: + dependency: transitive + description: + name: fwfh_svg + sha256: "2e6bb241179eeeb1a7941e05c8c923b05d332d36a9085233e7bf110ea7deb915" + url: "https://pub.dev" + source: hosted + version: "0.16.1" + fwfh_url_launcher: + dependency: transitive + description: + name: fwfh_url_launcher + sha256: c38aa8fb373fda3a89b951fa260b539f623f6edb45eee7874cb8b492471af881 + url: "https://pub.dev" + source: hosted + version: "0.16.1" + fwfh_webview: + dependency: transitive + description: + name: fwfh_webview + sha256: f71b0aa16e15d82f3c017f33560201ff5ae04e91e970cab5d12d3bcf970b870c + url: "https://pub.dev" + source: hosted + version: "0.15.6" + geoclue: + dependency: transitive + description: + name: geoclue + sha256: c2a998c77474fc57aa00c6baa2928e58f4b267649057a1c76738656e9dbd2a7f + url: "https://pub.dev" + source: hosted + version: "0.1.1" + geolocator: + dependency: "direct main" + description: + name: geolocator + sha256: "79939537046c9025be47ec645f35c8090ecadb6fe98eba146a0d25e8c1357516" + url: "https://pub.dev" + source: hosted + version: "14.0.2" + geolocator_android: + dependency: transitive + description: + name: geolocator_android + sha256: "179c3cb66dfa674fc9ccbf2be872a02658724d1c067634e2c427cf6df7df901a" + url: "https://pub.dev" + source: hosted + version: "5.0.2" + geolocator_apple: + dependency: transitive + description: + name: geolocator_apple + sha256: dbdd8789d5aaf14cf69f74d4925ad1336b4433a6efdf2fce91e8955dc921bf22 + url: "https://pub.dev" + source: hosted + version: "2.3.13" + geolocator_linux: + dependency: transitive + description: + name: geolocator_linux + sha256: d64112a205931926f4363bb6bd48f14cb38e7326833041d170615586cd143797 + url: "https://pub.dev" + source: hosted + version: "0.2.4" + geolocator_platform_interface: + dependency: transitive + description: + name: geolocator_platform_interface + sha256: "30cb64f0b9adcc0fb36f628b4ebf4f731a2961a0ebd849f4b56200205056fe67" + url: "https://pub.dev" + source: hosted + version: "4.2.6" + geolocator_web: + dependency: transitive + description: + name: geolocator_web + sha256: b1ae9bdfd90f861fde8fd4f209c37b953d65e92823cb73c7dee1fa021b06f172 + url: "https://pub.dev" + source: hosted + version: "4.1.3" + geolocator_windows: + dependency: transitive + description: + name: geolocator_windows + sha256: "175435404d20278ffd220de83c2ca293b73db95eafbdc8131fe8609be1421eb6" + url: "https://pub.dev" + source: hosted + version: "0.2.5" + get_it: + dependency: "direct main" + description: + name: get_it + sha256: ae78de7c3f2304b8d81f2bb6e320833e5e81de942188542328f074978cc0efa9 + url: "https://pub.dev" + source: hosted + version: "8.3.0" + gms_check: + dependency: "direct main" + description: + name: gms_check + sha256: b3fc08fd41da233f9761f9981303346aa9778b4802e90ce9bd8122674fcca6f0 + url: "https://pub.dev" + source: hosted + version: "1.0.4" + google_api_availability: + dependency: "direct main" + description: + name: google_api_availability + sha256: "2ffdc91e1e0cf4e7974fef6c2988a24cefa81f03526ff04b694df6dc0fcbca03" + url: "https://pub.dev" + source: hosted + version: "5.0.1" + google_api_availability_android: + dependency: transitive + description: + name: google_api_availability_android + sha256: "4794147f43a8f3eee6b514d3ae30dbe6f7b9048cae8cd2a74cb4055cd28d74a8" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + google_api_availability_platform_interface: + dependency: transitive + description: + name: google_api_availability_platform_interface + sha256: "65b7da62fe5b582bb3d508628ad827d36d890710ea274766a992a56fa5420da6" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + google_maps: + dependency: transitive + description: + name: google_maps + sha256: "5d410c32112d7c6eb7858d359275b2aa04778eed3e36c745aeae905fb2fa6468" + url: "https://pub.dev" + source: hosted + version: "8.2.0" + google_maps_flutter: + dependency: "direct main" + description: + name: google_maps_flutter + sha256: "819985697596a42e1054b5feb2f407ba1ac92262e02844a40168e742b9f36dca" + url: "https://pub.dev" + source: hosted + version: "2.14.0" + google_maps_flutter_android: + dependency: transitive + description: + name: google_maps_flutter_android + sha256: "6dbbfc697eedd29c3634affb2d6b3e5ecfc4e6e50c8345f4b975cc969c74b582" + url: "https://pub.dev" + source: hosted + version: "2.18.9" + google_maps_flutter_ios: + dependency: transitive + description: + name: google_maps_flutter_ios + sha256: b3f9aa62f65f7f266651e156a910ce88b8158de6546c6b145c9ba8080eb861b3 + url: "https://pub.dev" + source: hosted + version: "2.16.1" + google_maps_flutter_platform_interface: + dependency: transitive + description: + name: google_maps_flutter_platform_interface + sha256: e8b1232419fcdd35c1fdafff96843f5a40238480365599d8ca661dde96d283dd + url: "https://pub.dev" + source: hosted + version: "2.14.1" + google_maps_flutter_web: + dependency: transitive + description: + name: google_maps_flutter_web + sha256: d416602944e1859f3cbbaa53e34785c223fa0a11eddb34a913c964c5cbb5d8cf + url: "https://pub.dev" + source: hosted + version: "0.5.14+3" + gsettings: + dependency: transitive + description: + name: gsettings + sha256: "1b0ce661f5436d2db1e51f3c4295a49849f03d304003a7ba177d01e3a858249c" + url: "https://pub.dev" + source: hosted + version: "0.2.8" + health: + dependency: "direct main" + description: + name: health + sha256: "320633022fb2423178baa66508001c4ca5aee5806ffa2c913e66488081e9fd47" + url: "https://pub.dev" + source: hosted + version: "13.1.4" + hijri_gregorian_calendar: + dependency: "direct main" + description: + name: hijri_gregorian_calendar + sha256: aecdbe3c9365fac55f17b5e1f24086a81999b1e5c9372cb08888bfbe61e07fa1 + url: "https://pub.dev" + source: hosted + version: "0.1.1" + html: + dependency: transitive + description: + name: html + sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602" + url: "https://pub.dev" + source: hosted + version: "0.15.6" + http: + dependency: "direct main" + description: + name: http + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" + url: "https://pub.dev" + source: hosted + version: "1.6.0" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + huawei_health: + dependency: "direct main" + description: + name: huawei_health + sha256: "52fb9990e1fc857e2fa1b1251dde63b2146086a13b2d9c50bdfc3c4f715c8a12" + url: "https://pub.dev" + source: hosted + version: "6.16.0+300" + huawei_location: + dependency: "direct main" + description: + name: huawei_location + sha256: dd939b0add3e228865cb7da230d7723551e55677d7d59de7dbfd466229847b9f + url: "https://pub.dev" + source: hosted + version: "6.16.0+300" + huawei_map: + dependency: "direct main" + description: + path: flutter-hms-map + ref: HEAD + resolved-ref: "9a16541e4016e3bf58a2571e6aa658a4751af399" + url: "https://github.com/fleoparra/hms-flutter-plugin.git" + source: git + version: "6.11.2+303" + image_picker: + dependency: "direct main" + description: + name: image_picker + sha256: "784210112be18ea55f69d7076e2c656a4e24949fa9e76429fe53af0c0f4fa320" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + image_picker_android: + dependency: transitive + description: + name: image_picker_android + sha256: "5e9bf126c37c117cf8094215373c6d561117a3cfb50ebc5add1a61dc6e224677" + url: "https://pub.dev" + source: hosted + version: "0.8.13+10" + image_picker_for_web: + dependency: transitive + description: + name: image_picker_for_web + sha256: "66257a3191ab360d23a55c8241c91a6e329d31e94efa7be9cf7a212e65850214" + url: "https://pub.dev" + source: hosted + version: "3.1.1" + image_picker_ios: + dependency: transitive + description: + name: image_picker_ios + sha256: "956c16a42c0c708f914021666ffcd8265dde36e673c9fa68c81f7d085d9774ad" + url: "https://pub.dev" + source: hosted + version: "0.8.13+3" + image_picker_linux: + dependency: transitive + description: + name: image_picker_linux + sha256: "1f81c5f2046b9ab724f85523e4af65be1d47b038160a8c8deed909762c308ed4" + url: "https://pub.dev" + source: hosted + version: "0.2.2" + image_picker_macos: + dependency: transitive + description: + name: image_picker_macos + sha256: "86f0f15a309de7e1a552c12df9ce5b59fe927e71385329355aec4776c6a8ec91" + url: "https://pub.dev" + source: hosted + version: "0.2.2+1" + image_picker_platform_interface: + dependency: transitive + description: + name: image_picker_platform_interface + sha256: "567e056716333a1647c64bb6bd873cff7622233a5c3f694be28a583d4715690c" + url: "https://pub.dev" + source: hosted + version: "2.11.1" + image_picker_windows: + dependency: transitive + description: + name: image_picker_windows + sha256: d248c86554a72b5495a31c56f060cf73a41c7ff541689327b1a7dbccc33adfae + url: "https://pub.dev" + source: hosted + version: "0.2.2" + in_app_update: + dependency: "direct main" + description: + name: in_app_update + sha256: "9924a3efe592e1c0ec89dda3683b3cfec3d4cd02d908e6de00c24b759038ddb1" + url: "https://pub.dev" + source: hosted + version: "4.2.5" + intl: + dependency: "direct main" + description: + name: intl + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + url: "https://pub.dev" + source: hosted + version: "0.20.2" + jiffy: + dependency: "direct main" + description: + name: jiffy + sha256: e6f3b2aaec032f95ae917268edcbf007a5b834b57a602d39eb0ab17995a9c64a + url: "https://pub.dev" + source: hosted + version: "6.4.4" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" + url: "https://pub.dev" + source: hosted + version: "4.9.0" + just_audio: + dependency: "direct main" + description: + name: just_audio + sha256: "9694e4734f515f2a052493d1d7e0d6de219ee0427c7c29492e246ff32a219908" + url: "https://pub.dev" + source: hosted + version: "0.10.5" + just_audio_platform_interface: + dependency: transitive + description: + name: just_audio_platform_interface + sha256: "2532c8d6702528824445921c5ff10548b518b13f808c2e34c2fd54793b999a6a" + url: "https://pub.dev" + source: hosted + version: "4.6.0" + just_audio_web: + dependency: transitive + description: + name: just_audio_web + sha256: "6ba8a2a7e87d57d32f0f7b42856ade3d6a9fbe0f1a11fabae0a4f00bb73f0663" + url: "https://pub.dev" + source: hosted + version: "0.4.16" + keyboard_actions: + dependency: "direct main" + description: + name: keyboard_actions + sha256: "5155a158c0d22c3a2f4a2192040445fe84977620cf0eeb29f6148a1dcb5835fa" + url: "https://pub.dev" + source: hosted + version: "4.2.1" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.dev" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + lints: + dependency: transitive + description: + name: lints + sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 + url: "https://pub.dev" + source: hosted + version: "5.1.1" + local_auth: + dependency: "direct main" + description: + name: local_auth + sha256: "434d854cf478f17f12ab29a76a02b3067f86a63a6d6c4eb8fbfdcfe4879c1b7b" + url: "https://pub.dev" + source: hosted + version: "2.3.0" + local_auth_android: + dependency: transitive + description: + name: local_auth_android + sha256: a0bdfcc0607050a26ef5b31d6b4b254581c3d3ce3c1816ab4d4f4a9173e84467 + url: "https://pub.dev" + source: hosted + version: "1.0.56" + local_auth_darwin: + dependency: transitive + description: + name: local_auth_darwin + sha256: "699873970067a40ef2f2c09b4c72eb1cfef64224ef041b3df9fdc5c4c1f91f49" + url: "https://pub.dev" + source: hosted + version: "1.6.1" + local_auth_platform_interface: + dependency: transitive + description: + name: local_auth_platform_interface + sha256: f98b8e388588583d3f781f6806e4f4c9f9e189d898d27f0c249b93a1973dd122 + url: "https://pub.dev" + source: hosted + version: "1.1.0" + local_auth_windows: + dependency: transitive + description: + name: local_auth_windows + sha256: bc4e66a29b0fdf751aafbec923b5bed7ad6ed3614875d8151afe2578520b2ab5 + url: "https://pub.dev" + source: hosted + version: "1.0.11" + location: + dependency: "direct main" + description: + name: location + sha256: b080053c181c7d152c43dd576eec6436c40e25f326933051c330da563ddd5333 + url: "https://pub.dev" + source: hosted + version: "8.0.1" + location_platform_interface: + dependency: transitive + description: + name: location_platform_interface + sha256: ca8700bb3f6b1e8b2afbd86bd78b2280d116c613ca7bfa1d4d7b64eba357d749 + url: "https://pub.dev" + source: hosted + version: "6.0.1" + location_web: + dependency: transitive + description: + name: location_web + sha256: b8e3add5efe0d65c5e692b7a135d80a4015c580d3ea646fa71973e97668dd868 + url: "https://pub.dev" + source: hosted + version: "6.0.1" + logger: + dependency: "direct main" + description: + name: logger + sha256: a7967e31b703831a893bbc3c3dd11db08126fe5f369b5c648a36f821979f5be3 + url: "https://pub.dev" + source: hosted + version: "2.6.2" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + lottie: + dependency: "direct main" + description: + name: lottie + sha256: "8ae0be46dbd9e19641791dc12ee480d34e1fd3f84c749adc05f3ad9342b71b95" + url: "https://pub.dev" + source: hosted + version: "3.3.2" + manage_calendar_events: + dependency: "direct main" + description: + name: manage_calendar_events + sha256: f17600fcb7dc7047120c185993045e493d686930237b4e3c2689c26a64513d66 + url: "https://pub.dev" + source: hosted + version: "2.0.3" + map_launcher: + dependency: "direct main" + description: + name: map_launcher + sha256: "85ae218777b79c830477ed59d97f5ee9d6025b00c47b05d0b901f4dd7d2297cc" + url: "https://pub.dev" + source: hosted + version: "4.4.3" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + url: "https://pub.dev" + source: hosted + version: "0.12.17" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.dev" + source: hosted + version: "0.11.1" + meta: + dependency: transitive + description: + name: meta + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + url: "https://pub.dev" + source: hosted + version: "1.16.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + ndef: + dependency: transitive + description: + name: ndef + sha256: "198ba3798e80cea381648569d84059dbba64cd140079fb7b0d9c3f1e0f5973f3" + url: "https://pub.dev" + source: hosted + version: "0.4.0" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + network_info_plus: + dependency: "direct main" + description: + name: network_info_plus + sha256: f926b2ba86aa0086a0dfbb9e5072089bc213d854135c1712f1d29fc89ba3c877 + url: "https://pub.dev" + source: hosted + version: "6.1.4" + network_info_plus_platform_interface: + dependency: transitive + description: + name: network_info_plus_platform_interface + sha256: "7e7496a8a9d8136859b8881affc613c4a21304afeb6c324bcefc4bd0aff6b94b" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + nm: + dependency: transitive + description: + name: nm + sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254" + url: "https://pub.dev" + source: hosted + version: "0.5.0" + octo_image: + dependency: transitive + description: + name: octo_image + sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + open_filex: + dependency: "direct main" + description: + name: open_filex + sha256: "9976da61b6a72302cf3b1efbce259200cd40232643a467aac7370addf94d6900" + url: "https://pub.dev" + source: hosted + version: "4.7.0" + package_info_plus: + dependency: transitive + description: + name: package_info_plus + sha256: f69da0d3189a4b4ceaeb1a3defb0f329b3b352517f52bed4290f83d4f06bc08d + url: "https://pub.dev" + source: hosted + version: "9.0.0" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + path_parsing: + dependency: transitive + description: + name: path_parsing + sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + path_provider: + dependency: "direct main" + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e + url: "https://pub.dev" + source: hosted + version: "2.2.22" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "6d13aece7b3f5c5a9731eaf553ff9dcbc2eff41087fd2df587fd0fed9a3eb0c4" + url: "https://pub.dev" + source: hosted + version: "2.5.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + permission_handler: + dependency: "direct main" + description: + name: permission_handler + sha256: bc917da36261b00137bbc8896bf1482169cd76f866282368948f032c8c1caae1 + url: "https://pub.dev" + source: hosted + version: "12.0.1" + permission_handler_android: + dependency: transitive + description: + name: permission_handler_android + sha256: "1e3bc410ca1bf84662104b100eb126e066cb55791b7451307f9708d4007350e6" + url: "https://pub.dev" + source: hosted + version: "13.0.1" + permission_handler_apple: + dependency: transitive + description: + name: permission_handler_apple + sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023 + url: "https://pub.dev" + source: hosted + version: "9.4.7" + permission_handler_html: + dependency: transitive + description: + name: permission_handler_html + sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24" + url: "https://pub.dev" + source: hosted + version: "0.1.3+5" + permission_handler_platform_interface: + dependency: transitive + description: + name: permission_handler_platform_interface + sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878 + url: "https://pub.dev" + source: hosted + version: "4.3.0" + permission_handler_windows: + dependency: transitive + description: + name: permission_handler_windows + sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e" + url: "https://pub.dev" + source: hosted + version: "0.2.1" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1" + url: "https://pub.dev" + source: hosted + version: "7.0.1" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + pointycastle: + dependency: transitive + description: + name: pointycastle + sha256: "92aa3841d083cc4b0f4709b5c74fd6409a3e6ba833ffc7dc6a8fee096366acf5" + url: "https://pub.dev" + source: hosted + version: "4.0.0" + posix: + dependency: transitive + description: + name: posix + sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61" + url: "https://pub.dev" + source: hosted + version: "6.0.3" + protobuf: + dependency: transitive + description: + name: protobuf + sha256: "75ec242d22e950bdcc79ee38dd520ce4ee0bc491d7fadc4ea47694604d22bf06" + url: "https://pub.dev" + source: hosted + version: "6.0.0" + provider: + dependency: "direct main" + description: + name: provider + sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272" + url: "https://pub.dev" + source: hosted + version: "6.1.5+1" + quiver: + dependency: transitive + description: + name: quiver + sha256: ea0b925899e64ecdfbf9c7becb60d5b50e706ade44a85b2363be2a22d88117d2 + url: "https://pub.dev" + source: hosted + version: "3.2.2" + rrule: + dependency: transitive + description: + name: rrule + sha256: f6f6ad5bf7b19d218d4c985d6055d3c9717f1d6efd5d1c0127b1146f1eb3640c + url: "https://pub.dev" + source: hosted + version: "0.2.18" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" + url: "https://pub.dev" + source: hosted + version: "0.28.0" + sanitize_html: + dependency: transitive + description: + name: sanitize_html + sha256: "12669c4a913688a26555323fb9cec373d8f9fbe091f2d01c40c723b33caa8989" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + scrollable_positioned_list: + dependency: "direct main" + description: + name: scrollable_positioned_list + sha256: "1b54d5f1329a1e263269abc9e2543d90806131aa14fe7c6062a8054d57249287" + url: "https://pub.dev" + source: hosted + version: "0.3.8" + share_plus: + dependency: "direct main" + description: + name: share_plus + sha256: d7dc0630a923883c6328ca31b89aa682bacbf2f8304162d29f7c6aaff03a27a1 + url: "https://pub.dev" + source: hosted + version: "11.1.0" + share_plus_platform_interface: + dependency: transitive + description: + name: share_plus_platform_interface + sha256: "88023e53a13429bd65d8e85e11a9b484f49d4c190abbd96c7932b74d6927cc9a" + url: "https://pub.dev" + source: hosted + version: "6.1.0" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: "2939ae520c9024cb197fc20dee269cd8cdbf564c8b5746374ec6cacdc5169e64" + url: "https://pub.dev" + source: hosted + version: "2.5.4" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "83af5c682796c0f7719c2bbf74792d113e40ae97981b8f266fa84574573556bc" + url: "https://pub.dev" + source: hosted + version: "2.4.18" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f" + url: "https://pub.dev" + source: hosted + version: "2.5.6" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 + url: "https://pub.dev" + source: hosted + version: "2.4.3" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shimmer: + dependency: "direct main" + description: + name: shimmer + sha256: "5f88c883a22e9f9f299e5ba0e4f7e6054857224976a5d9f839d4ebdc94a14ac9" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + sizer: + dependency: "direct main" + description: + name: sizer + sha256: "9963c89e4d30d7c2108de3eafc0a7e6a4a8009799376ea6be5ef0a9ad87cfbad" + url: "https://pub.dev" + source: hosted + version: "3.1.3" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + smooth_corner: + dependency: "direct main" + description: + name: smooth_corner + sha256: "112d7331f82ead81ec870c5d1eb0624f2e7e367eccd166c2fffe4c11d4f87c4f" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + sms_otp_auto_verify: + dependency: "direct main" + description: + name: sms_otp_auto_verify + sha256: ee02af0d6b81d386ef70d7d0317a1929bc0b4a3a30a451284450bbcf6901ba1a + url: "https://pub.dev" + source: hosted + version: "2.2.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" + url: "https://pub.dev" + source: hosted + version: "1.10.1" + sqflite: + dependency: transitive + description: + name: sqflite + sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03 + url: "https://pub.dev" + source: hosted + version: "2.4.2" + sqflite_android: + dependency: transitive + description: + name: sqflite_android + sha256: ecd684501ebc2ae9a83536e8b15731642b9570dc8623e0073d227d0ee2bfea88 + url: "https://pub.dev" + source: hosted + version: "2.4.2+2" + sqflite_common: + dependency: transitive + description: + name: sqflite_common + sha256: "6ef422a4525ecc601db6c0a2233ff448c731307906e92cabc9ba292afaae16a6" + url: "https://pub.dev" + source: hosted + version: "2.5.6" + sqflite_darwin: + dependency: transitive + description: + name: sqflite_darwin + sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + sqflite_platform_interface: + dependency: transitive + description: + name: sqflite_platform_interface + sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920" + url: "https://pub.dev" + source: hosted + version: "2.4.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 + url: "https://pub.dev" + source: hosted + version: "2.1.1" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + syncfusion_flutter_calendar: + dependency: "direct main" + description: + name: syncfusion_flutter_calendar + sha256: "8e8a4eef01d6a82ae2c17e76d497ff289ded274de014c9f471ffabc12d1e2e71" + url: "https://pub.dev" + source: hosted + version: "30.2.7" + syncfusion_flutter_core: + dependency: transitive + description: + name: syncfusion_flutter_core + sha256: bfd026c0f9822b49ff26fed11cd3334519acb6a6ad4b0c81d9cd18df6af1c4c0 + url: "https://pub.dev" + source: hosted + version: "30.2.7" + syncfusion_flutter_datepicker: + dependency: transitive + description: + name: syncfusion_flutter_datepicker + sha256: b5f35cc808e91b229d41613efe71dadab1549a35bfd493f922fc06ccc2fe908c + url: "https://pub.dev" + source: hosted + version: "30.2.7" + syncfusion_localizations: + dependency: transitive + description: + name: syncfusion_localizations + sha256: bb32b07879b4c1dee5d4c8ad1c57343a4fdae55d65a87f492727c11b68f23164 + url: "https://pub.dev" + source: hosted + version: "30.2.7" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0 + url: "https://pub.dev" + source: hosted + version: "3.4.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" + url: "https://pub.dev" + source: hosted + version: "0.7.6" + time: + dependency: transitive + description: + name: time + sha256: "46187cf30bffdab28c56be9a63861b36e4ab7347bf403297595d6a97e10c789f" + url: "https://pub.dev" + source: hosted + version: "2.1.6" + timezone: + dependency: "direct main" + description: + name: timezone + sha256: dd14a3b83cfd7cb19e7888f1cbc20f258b8d71b54c06f79ac585f14093a287d1 + url: "https://pub.dev" + source: hosted + version: "0.10.1" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + universal_platform: + dependency: transitive + description: + name: universal_platform + sha256: "64e16458a0ea9b99260ceb5467a214c1f298d647c659af1bff6d3bf82536b1ec" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 + url: "https://pub.dev" + source: hosted + version: "6.3.2" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "767344bf3063897b5cf0db830e94f904528e6dd50a6dfaf839f0abf509009611" + url: "https://pub.dev" + source: hosted + version: "6.3.28" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: cfde38aa257dae62ffe79c87fab20165dfdf6988c1d31b58ebf59b9106062aad + url: "https://pub.dev" + source: hosted + version: "6.3.6" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a + url: "https://pub.dev" + source: hosted + version: "3.2.2" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18" + url: "https://pub.dev" + source: hosted + version: "3.2.5" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f" + url: "https://pub.dev" + source: hosted + version: "3.1.5" + uuid: + dependency: "direct main" + description: + name: uuid + sha256: a11b666489b1954e01d992f3d601b1804a33937b5a8fe677bd26b8a9f96f96e8 + url: "https://pub.dev" + source: hosted + version: "4.5.2" + vector_graphics: + dependency: transitive + description: + name: vector_graphics + sha256: a4f059dc26fc8295b5921376600a194c4ec7d55e72f2fe4c7d2831e103d461e6 + url: "https://pub.dev" + source: hosted + version: "1.1.19" + vector_graphics_codec: + dependency: transitive + description: + name: vector_graphics_codec + sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146" + url: "https://pub.dev" + source: hosted + version: "1.1.13" + vector_graphics_compiler: + dependency: transitive + description: + name: vector_graphics_compiler + sha256: d354a7ec6931e6047785f4db12a1f61ec3d43b207fc0790f863818543f8ff0dc + url: "https://pub.dev" + source: hosted + version: "1.1.19" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.dev" + source: hosted + version: "2.2.0" + video_player: + dependency: transitive + description: + name: video_player + sha256: "096bc28ce10d131be80dfb00c223024eb0fba301315a406728ab43dd99c45bdf" + url: "https://pub.dev" + source: hosted + version: "2.10.1" + video_player_android: + dependency: transitive + description: + name: video_player_android + sha256: ee4fd520b0cafa02e4a867a0f882092e727cdaa1a2d24762171e787f8a502b0a + url: "https://pub.dev" + source: hosted + version: "2.9.1" + video_player_avfoundation: + dependency: transitive + description: + name: video_player_avfoundation + sha256: d1eb970495a76abb35e5fa93ee3c58bd76fb6839e2ddf2fbb636674f2b971dd4 + url: "https://pub.dev" + source: hosted + version: "2.8.9" + video_player_platform_interface: + dependency: transitive + description: + name: video_player_platform_interface + sha256: "57c5d73173f76d801129d0531c2774052c5a7c11ccb962f1830630decd9f24ec" + url: "https://pub.dev" + source: hosted + version: "6.6.0" + video_player_web: + dependency: transitive + description: + name: video_player_web + sha256: "9f3c00be2ef9b76a95d94ac5119fb843dca6f2c69e6c9968f6f2b6c9e7afbdeb" + url: "https://pub.dev" + source: hosted + version: "2.4.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" + url: "https://pub.dev" + source: hosted + version: "15.0.2" + wakelock_plus: + dependency: transitive + description: + name: wakelock_plus + sha256: "9296d40c9adbedaba95d1e704f4e0b434be446e2792948d0e4aa977048104228" + url: "https://pub.dev" + source: hosted + version: "1.4.0" + wakelock_plus_platform_interface: + dependency: transitive + description: + name: wakelock_plus_platform_interface + sha256: "036deb14cd62f558ca3b73006d52ce049fabcdcb2eddfe0bf0fe4e8a943b5cf2" + url: "https://pub.dev" + source: hosted + version: "1.3.0" + web: + dependency: "direct main" + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + webview_flutter: + dependency: transitive + description: + name: webview_flutter + sha256: a3da219916aba44947d3a5478b1927876a09781174b5a2b67fa5be0555154bf9 + url: "https://pub.dev" + source: hosted + version: "4.13.1" + webview_flutter_android: + dependency: transitive + description: + name: webview_flutter_android + sha256: eeeb3fcd5f0ff9f8446c9f4bbc18a99b809e40297528a3395597d03aafb9f510 + url: "https://pub.dev" + source: hosted + version: "4.10.11" + webview_flutter_platform_interface: + dependency: transitive + description: + name: webview_flutter_platform_interface + sha256: "63d26ee3aca7256a83ccb576a50272edd7cfc80573a4305caa98985feb493ee0" + url: "https://pub.dev" + source: hosted + version: "2.14.0" + webview_flutter_wkwebview: + dependency: transitive + description: + name: webview_flutter_wkwebview + sha256: e49f378ed066efb13fc36186bbe0bd2425630d4ea0dbc71a18fdd0e4d8ed8ebc + url: "https://pub.dev" + source: hosted + version: "3.23.5" + win32: + dependency: transitive + description: + name: win32 + sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e + url: "https://pub.dev" + source: hosted + version: "5.15.0" + win32_registry: + dependency: transitive + description: + name: win32_registry + sha256: "6f1b564492d0147b330dd794fee8f512cec4977957f310f9951b5f9d83618dae" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + xml: + dependency: transitive + description: + name: xml + sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" + url: "https://pub.dev" + source: hosted + version: "6.6.1" +sdks: + dart: ">=3.9.0 <4.0.0" + flutter: ">=3.35.0" diff --git a/pubspec.yaml b/pubspec.yaml index 4484fa77..3b112673 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,8 +2,8 @@ name: hmg_patient_app_new description: "New HMG Patient App" publish_to: 'none' # Remove this line if you wish to publish to pub.dev -version: 0.0.15+12 -#version: 0.0.1+14 +#version: 0.0.15+12 +version: 0.0.1+16 environment: sdk: ">=3.6.0 <4.0.0" @@ -92,13 +92,15 @@ dependencies: location: ^8.0.1 gms_check: ^1.0.4 huawei_location: ^6.14.2+301 -# huawei_health: ^6.16.0+300 + huawei_health: ^6.15.0+300 intl: ^0.20.2 flutter_widget_from_html: ^0.17.1 huawei_map: ^6.12.0+301 scrollable_positioned_list: ^0.3.8 + in_app_review: ^2.0.11 + dev_dependencies: flutter_test: sdk: flutter