diff --git a/assets/json/default_occasion_config.json b/assets/json/default_occasion_config.json new file mode 100644 index 00000000..6b38a4cb --- /dev/null +++ b/assets/json/default_occasion_config.json @@ -0,0 +1,12 @@ +{ + "key": "none", + "enabled": false, + "start_date": null, + "end_date": null, + "primary_color": null, + "secondary_color": null, + "background_opacity_image": null, + "background_image_opacity": 0.2, + "header_image": null +} + diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist index 1dc6cf76..391a902b 100644 --- a/ios/Flutter/AppFrameworkInfo.plist +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -20,7 +20,5 @@ ???? CFBundleVersion 1.0 - MinimumOSVersion - 13.0 diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index e3773d42..c3fedb29 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -5,6 +5,24 @@ + + + + + + + + + + 252 ? '...${exceptionMsg.substring(exceptionMsg.length - 252)}' : exceptionMsg; Clarity.setCustomTag('api_err_exception', truncatedMsg); - + if (_appState.isAuthenticated) { Clarity.setCustomTag('api_err_user_id', _appState.getAuthenticatedUser()!.patientId.toString()); } - + Clarity.sendCustomEvent('api_unknown_exception'); - + return; } @@ -384,24 +383,19 @@ class ApiClientImp implements ApiClient { onFailure( parsed['ErrorEndUserMessage'] ?? parsed['ErrorMessage'], statusCode, - failureType: - UnAuthenticatedUserFailure(parsed['ErrorEndUserMessage'] ?? parsed['ErrorMessage'] ?? "User is not Authenticated", url: url), + failureType: UnAuthenticatedUserFailure(parsed['ErrorEndUserMessage'] ?? parsed['ErrorMessage'] ?? "User is not Authenticated", url: url), ); // logApiEndpointError(endPoint, "session logged out", statusCode); } if (isAllowAny) { - onSuccess(parsed, statusCode, - messageStatus: parsed['MessageStatus'], errorMessage: parsed['ErrorEndUserMessage'] ?? parsed['ErrorMessage']); + onSuccess(parsed, statusCode, messageStatus: parsed['MessageStatus'], errorMessage: parsed['ErrorEndUserMessage'] ?? parsed['ErrorMessage']); } else if (parsed['IsAuthenticated'] == null) { if (parsed['isSMSSent'] == true) { - onSuccess(parsed, statusCode, - messageStatus: parsed['MessageStatus'], errorMessage: parsed['ErrorEndUserMessage'] ?? parsed['ErrorMessage']); + onSuccess(parsed, statusCode, messageStatus: parsed['MessageStatus'], errorMessage: parsed['ErrorEndUserMessage'] ?? parsed['ErrorMessage']); } else if (parsed['MessageStatus'] == 1) { - onSuccess(parsed, statusCode, - messageStatus: parsed['MessageStatus'], errorMessage: parsed['ErrorEndUserMessage'] ?? parsed['ErrorMessage']); + onSuccess(parsed, statusCode, messageStatus: parsed['MessageStatus'], errorMessage: parsed['ErrorEndUserMessage'] ?? parsed['ErrorMessage']); } else if (parsed['Result'] == 'OK') { - onSuccess(parsed, statusCode, - messageStatus: parsed['MessageStatus'], errorMessage: parsed['ErrorEndUserMessage'] ?? parsed['ErrorMessage']); + onSuccess(parsed, statusCode, messageStatus: parsed['MessageStatus'], errorMessage: parsed['ErrorEndUserMessage'] ?? parsed['ErrorMessage']); } else { onFailure( parsed['ErrorEndUserMessage'] ?? parsed['ErrorMessage'], @@ -411,8 +405,7 @@ class ApiClientImp implements ApiClient { logApiEndpointError(endPoint, parsed['ErrorEndUserMessage'] ?? parsed['ErrorMessage'], statusCode); } } else if (parsed['MessageStatus'] == 1 || parsed['SMSLoginRequired'] == true) { - onSuccess(parsed, statusCode, - messageStatus: parsed['MessageStatus'], errorMessage: parsed['ErrorEndUserMessage'] ?? parsed['ErrorMessage']); + onSuccess(parsed, statusCode, messageStatus: parsed['MessageStatus'], errorMessage: parsed['ErrorEndUserMessage'] ?? parsed['ErrorMessage']); } // else if (parsed['IsAuthenticated'] == false) { // onFailure( @@ -424,8 +417,7 @@ class ApiClientImp implements ApiClient { // } else if (parsed['MessageStatus'] == 2 && parsed['IsAuthenticated']) { if (parsed['SameClinicApptList'] != null) { - onSuccess(parsed, statusCode, - messageStatus: parsed['MessageStatus'], errorMessage: parsed['ErrorEndUserMessage'] ?? parsed['ErrorMessage']); + onSuccess(parsed, statusCode, messageStatus: parsed['MessageStatus'], errorMessage: parsed['ErrorEndUserMessage'] ?? parsed['ErrorMessage']); } else { if (parsed['message'] == null && parsed['ErrorEndUserMessage'] == null) { if (parsed['ErrorSearchMsg'] == null) { @@ -459,8 +451,7 @@ class ApiClientImp implements ApiClient { } } else { if (parsed['SameClinicApptList'] != null) { - onSuccess(parsed, statusCode, - messageStatus: parsed['MessageStatus'], errorMessage: parsed['ErrorEndUserMessage'] ?? parsed['ErrorMessage']); + onSuccess(parsed, statusCode, messageStatus: parsed['MessageStatus'], errorMessage: parsed['ErrorEndUserMessage'] ?? parsed['ErrorMessage']); } else { if (parsed['message'] != null) { onFailure( @@ -537,74 +528,74 @@ class ApiClientImp implements ApiClient { final message = e.message.contains('Connection reset by peer') ? LocaleKeys.networkConnectionReset.tr() : LocaleKeys.networkErrorMessage.tr(); onFailure(message, -1); _analytics.errorTracking.log(endPoint, error: "SocketException: $e"); - + // Clarity logging for GET requests Clarity.setCustomTag('api_err_type', 'socket_exception'); Clarity.setCustomTag('api_err_url', _getEndofUrl(url.trim())); Clarity.setCustomTag('api_err_endpoint', _getEndpointName(endPoint)); Clarity.setCustomTag('api_err_method', 'GET'); - + if (_appState.isAuthenticated) { Clarity.setCustomTag('api_err_user_id', _appState.getAuthenticatedUser()!.patientId.toString()); } - + Clarity.sendCustomEvent('api_get_socket_exception'); - + return; } on http.ClientException catch (e) { final message = e.message.contains('Connection reset by peer') ? LocaleKeys.networkConnectionReset.tr() : LocaleKeys.networkErrorMessage.tr(); onFailure(message, -1); _analytics.errorTracking.log(endPoint, error: "ClientException: $e"); - + Clarity.setCustomTag('api_err_type', 'client_exception'); Clarity.setCustomTag('api_err_url', _getEndofUrl(url.trim())); Clarity.setCustomTag('api_err_endpoint', _getEndpointName(endPoint)); Clarity.setCustomTag('api_err_method', 'GET'); - + if (_appState.isAuthenticated) { Clarity.setCustomTag('api_err_user_id', _appState.getAuthenticatedUser()!.patientId.toString()); } - + Clarity.sendCustomEvent('api_get_client_exception'); - + return; } on TimeoutException catch (_) { final message = LocaleKeys.networkTimeout.tr(); onFailure(message, -1); _analytics.errorTracking.log(endPoint, error: "TimeoutException"); - + Clarity.setCustomTag('api_err_type', 'timeout'); Clarity.setCustomTag('api_err_url', _getEndofUrl(url.trim())); Clarity.setCustomTag('api_err_endpoint', _getEndpointName(endPoint)); Clarity.setCustomTag('api_err_method', 'GET'); - + if (_appState.isAuthenticated) { Clarity.setCustomTag('api_err_user_id', _appState.getAuthenticatedUser()!.patientId.toString()); } - + Clarity.sendCustomEvent('api_get_timeout'); - + return; } catch (e) { final message = LocaleKeys.networkUnknownError.tr(); onFailure(message, -1); _analytics.errorTracking.log(endPoint, error: "UnknownException: $e"); - + Clarity.setCustomTag('api_err_type', 'unknown_exception'); Clarity.setCustomTag('api_err_url', _getEndofUrl(url.trim())); Clarity.setCustomTag('api_err_endpoint', _getEndpointName(endPoint)); Clarity.setCustomTag('api_err_method', 'GET'); - + final exceptionMsg = e.toString(); final truncatedMsg = exceptionMsg.length > 252 ? '...${exceptionMsg.substring(exceptionMsg.length - 252)}' : exceptionMsg; Clarity.setCustomTag('api_err_exception', truncatedMsg); - + if (_appState.isAuthenticated) { Clarity.setCustomTag('api_err_user_id', _appState.getAuthenticatedUser()!.patientId.toString()); } - + Clarity.sendCustomEvent('api_get_unknown_exception'); - + return; } @@ -614,18 +605,18 @@ class ApiClientImp implements ApiClient { if (statusCode < 200 || statusCode >= 400) { onFailure(LocaleKeys.networkErrorWhileFetching.tr(), statusCode); logApiEndpointError(endPoint, LocaleKeys.networkErrorWhileFetching.tr(), statusCode); - + // Clarity logging for GET status code errors Clarity.setCustomTag('api_err_type', 'status_code_error'); Clarity.setCustomTag('api_err_url', _getEndofUrl(url.trim())); Clarity.setCustomTag('api_err_endpoint', _getEndpointName(endPoint)); Clarity.setCustomTag('api_err_method', 'GET'); Clarity.setCustomTag('api_err_status', statusCode.toString()); - + if (_appState.isAuthenticated) { Clarity.setCustomTag('api_err_user_id', _appState.getAuthenticatedUser()!.patientId.toString()); } - + Clarity.sendCustomEvent('api_get_status_error'); } else { var parsed = json.decode(utf8.decode(response.bodyBytes)); @@ -813,7 +804,7 @@ class ApiClientImp implements ApiClient { // Extract the last part of the endpoint path for cleaner tracking const int clarityLimit = 100; final cleanEndpoint = endpoint.split('/').last; - + if (cleanEndpoint.length <= clarityLimit) return cleanEndpoint; return '...${cleanEndpoint.substring(cleanEndpoint.length - (clarityLimit - 3))}'; } diff --git a/lib/core/api_consts.dart b/lib/core/api_consts.dart index ea8f3341..9b73c871 100644 --- a/lib/core/api_consts.dart +++ b/lib/core/api_consts.dart @@ -1,11 +1,12 @@ import 'package:amazon_payfort/amazon_payfort.dart'; import 'package:flutter_amazonpaymentservices/environment_type.dart'; import 'package:hmg_patient_app_new/core/enums.dart'; +import 'package:myfatoorah_flutter/myfatoorah_flutter.dart'; class ApiConsts { static const maxSmallScreen = 660; - static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.prod; + static AppEnvironmentTypeEnum appEnvironmentType = AppEnvironmentTypeEnum.uat; // static String baseUrl = 'https://uat.hmgwebservices.com/'; // HIS API URL UAT @@ -44,12 +45,17 @@ class ApiConsts { // var payFortEnvironment = FortEnvironment.test; // var applePayMerchantId = "merchant.com.hmgwebservices.uat"; + static var myFatoorahEnvironmentType = MFEnvironment.TEST; + static var myFatoorahCountry = MFCountry.SAUDIARABIA; + static setBackendURLs() { switch (appEnvironmentType) { case AppEnvironmentTypeEnum.prod: baseUrl = "https://hmgwebservices.com/"; payFortEnvironment = FortEnvironment.production; payFortEnvironmentType = EnvironmentType.production; + myFatoorahEnvironmentType = MFEnvironment.LIVE; + myFatoorahCountry = MFCountry.SAUDIARABIA; applePayMerchantId = "merchant.com.hmgwebservices"; SERVICE_URL = "https://hmgwebservices.com/PayFortWebLive/pages/SendPayFortRequest.aspx"; TAMARA_URL = "https://mdlaboratories.com/tamaralive/Home/Checkout"; @@ -66,6 +72,8 @@ class ApiConsts { baseUrl = "https://uat.hmgwebservices.com/"; payFortEnvironment = FortEnvironment.test; payFortEnvironmentType = EnvironmentType.sandbox; + myFatoorahEnvironmentType = MFEnvironment.TEST; + myFatoorahCountry = MFCountry.KUWAIT; applePayMerchantId = "merchant.com.hmgwebservices.uat"; SERVICE_URL = 'https://uat.hmgwebservices.com/HMGPayment/pages/SendPayFortRequest.aspx'; TAMARA_URL = "https://epharmacy.hmg.com/tamara/Home/Checkout"; @@ -77,12 +85,13 @@ class ApiConsts { symptomsCheckerApi = symptomsCheckerApiUAT; symptomsCheckerUsername = 'guest_user'; symptomsCheckerPassword = '123456'; - break; case AppEnvironmentTypeEnum.uat: baseUrl = "https://uat.hmgwebservices.com/"; payFortEnvironment = FortEnvironment.test; payFortEnvironmentType = EnvironmentType.sandbox; + myFatoorahEnvironmentType = MFEnvironment.TEST; + myFatoorahCountry = MFCountry.KUWAIT; applePayMerchantId = "merchant.com.hmgwebservices.uat"; SERVICE_URL = 'https://uat.hmgwebservices.com/HMGPayment/pages/SendPayFortRequest.aspx'; TAMARA_URL = "https://epharmacy.hmg.com/tamara/Home/Checkout"; @@ -100,6 +109,8 @@ class ApiConsts { baseUrl = "https://webservices.hmg.com/"; payFortEnvironment = FortEnvironment.production; payFortEnvironmentType = EnvironmentType.production; + myFatoorahEnvironmentType = MFEnvironment.LIVE; + myFatoorahCountry = MFCountry.SAUDIARABIA; applePayMerchantId = "merchant.com.hmgwebservices"; SERVICE_URL = "https://hmgwebservices.com/PayFortWebLive/pages/SendPayFortRequest.aspx"; TAMARA_URL = "https://epharmacy.hmg.com/tamara/Home/Checkout"; @@ -117,6 +128,8 @@ class ApiConsts { baseUrl = "https://uat.hmgwebservices.com/"; payFortEnvironment = FortEnvironment.test; payFortEnvironmentType = EnvironmentType.sandbox; + myFatoorahEnvironmentType = MFEnvironment.TEST; + myFatoorahCountry = MFCountry.KUWAIT; applePayMerchantId = "merchant.com.hmgwebservices.uat"; SERVICE_URL = 'https://uat.hmgwebservices.com/HMGPayment/pages/SendPayFortRequest.aspx'; TAMARA_URL = "https://epharmacy.hmg.com/tamara/Home/Checkout"; @@ -134,6 +147,8 @@ class ApiConsts { baseUrl = "https://uat.hmgwebservices.com/"; payFortEnvironment = FortEnvironment.test; payFortEnvironmentType = EnvironmentType.sandbox; + myFatoorahEnvironmentType = MFEnvironment.TEST; + myFatoorahCountry = MFCountry.KUWAIT; applePayMerchantId = "merchant.com.hmgwebservices.uat"; SERVICE_URL = 'https://uat.hmgwebservices.com/HMGPayment/pages/SendPayFortRequest.aspx'; TAMARA_URL = "https://epharmacy.hmg.com/tamara/Home/Checkout"; @@ -865,8 +880,7 @@ var GET_CUSTOMER_INFO = "VerifyCustomer"; //Pharmacy -var GET_PHARMACY_CATEGORISE = - 'categories?fields=id,name,namen,description,image,localized_names,display_order,parent_category_id,is_leaf&parent_id=0'; +var GET_PHARMACY_CATEGORISE = 'categories?fields=id,name,namen,description,image,localized_names,display_order,parent_category_id,is_leaf&parent_id=0'; var GET_OFFERS_CATEGORISE = 'discountcategories'; var GET_OFFERS_PRODUCTS = 'offerproducts/'; var GET_CATEGORISE_PARENT = 'categories?fields=id,name,namen,description,image,localized_names,display_order,parent_category_id,is_leaf&parent_id='; diff --git a/lib/core/app_state.dart b/lib/core/app_state.dart index eace4b39..c1d0293d 100644 --- a/lib/core/app_state.dart +++ b/lib/core/app_state.dart @@ -48,12 +48,19 @@ class AppState { bool isPaytabsEnabled = false; bool isPayfortHostedPageEnabled = true; + bool isMyFatoorahEnabled = false; + // bool isMyFatoorahEnabled = true; void setIsPaytabsEnabled(bool value) { isPaytabsEnabled = value; // isPaytabsEnabled = true; } + void setIsMyFatoorahEnabled(bool value) { + // isMyFatoorahEnabled = value; + isMyFatoorahEnabled = true; + } + void setIsPayfortHostedPageEnabled(bool value) { isPayfortHostedPageEnabled = value; // isPayfortHostedPageEnabled = false; diff --git a/lib/core/dependencies.dart b/lib/core/dependencies.dart index b6c7fb38..97458e1b 100644 --- a/lib/core/dependencies.dart +++ b/lib/core/dependencies.dart @@ -44,6 +44,7 @@ import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_rep import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart'; import 'package:hmg_patient_app_new/features/my_invoices/my_invoices_repo.dart'; import 'package:hmg_patient_app_new/features/my_invoices/my_invoices_view_model.dart'; +import 'package:hmg_patient_app_new/features/myfatoorah/myfatoorah_view_model.dart'; import 'package:hmg_patient_app_new/features/notifications/notifications_repo.dart'; import 'package:hmg_patient_app_new/features/notifications/notifications_view_model.dart'; import 'package:hmg_patient_app_new/features/offers_and_discounts/offers_and_discounts_repo.dart'; @@ -84,7 +85,9 @@ import 'package:hmg_patient_app_new/services/logger_service.dart'; import 'package:hmg_patient_app_new/services/navigation_service.dart'; import 'package:hmg_patient_app_new/services/notification_service.dart'; import 'package:hmg_patient_app_new/services/permission_service.dart'; +import 'package:hmg_patient_app_new/services/remote_config_service.dart'; import 'package:hmg_patient_app_new/services/security_service.dart'; +import 'package:hmg_patient_app_new/features/occasion_theme/occasion_theme_view_model.dart'; import 'package:hmg_patient_app_new/core/services/turnstile_service.dart'; import 'package:hmg_patient_app_new/widgets/date_range_selector/viewmodel/date_range_calendar_model.dart'; import 'package:hmg_patient_app_new/widgets/date_range_selector/viewmodel/date_range_view_model.dart'; @@ -380,8 +383,22 @@ class AppDependencies { errorHandlerService: getIt(), )); + getIt.registerLazySingleton(() => MyFatoorahViewModel( + appState: getIt(), + errorHandlerService: getIt(), + payfortRepo: getIt(), + )); + getIt.registerLazySingleton( () => ClarityService(appState: getIt.get()), ); + + // Remote-config-driven "occasion" theming (National Day, Eid, etc.). + getIt.registerLazySingleton(() => RemoteConfigServiceImp(loggerService: getIt())); + getIt.registerLazySingleton(() => OccasionThemeViewModel( + remoteConfigService: getIt(), + cacheService: getIt(), + loggerService: getIt(), + )); } } diff --git a/lib/core/myFatoorah_config.dart b/lib/core/myFatoorah_config.dart new file mode 100644 index 00000000..582ced3c --- /dev/null +++ b/lib/core/myFatoorah_config.dart @@ -0,0 +1,5 @@ +class MyFatoorahConfig { + + static const String configAPIKey = "SK_KWT_0auB76h3Frhj7aUBSZa6H5ei792eZoGAPh0bBxKsrgtyjqKgf38MPnZqzGqagErb"; + +} \ No newline at end of file diff --git a/lib/core/utils/utils.dart b/lib/core/utils/utils.dart index d3107d72..0d09a716 100644 --- a/lib/core/utils/utils.dart +++ b/lib/core/utils/utils.dart @@ -437,8 +437,8 @@ class Utils { onCancelTap(); } }, - backgroundColor: AppColors.primaryRedColor, - borderColor: AppColors.primaryRedColor, + backgroundColor: AppColors.criticalActionColor, + borderColor: AppColors.criticalActionColor, textColor: AppColors.whiteColor, // icon: AppAssets.cancel, // iconColor: AppColors.whiteColor, @@ -516,8 +516,8 @@ class Utils { onCancelTap(); } }, - backgroundColor: AppColors.primaryRedColor, - borderColor: AppColors.primaryRedColor, + backgroundColor: AppColors.criticalActionColor, + borderColor: AppColors.criticalActionColor, textColor: AppColors.whiteColor, // icon: AppAssets.cancel, // iconColor: AppColors.whiteColor, diff --git a/lib/features/authentication/authentication_view_model.dart b/lib/features/authentication/authentication_view_model.dart index 3baecc40..b0764941 100644 --- a/lib/features/authentication/authentication_view_model.dart +++ b/lib/features/authentication/authentication_view_model.dart @@ -1248,6 +1248,7 @@ class AuthenticationViewModel extends ChangeNotifier { _appState.setUserBloodGroup = activation.patientBlodType ?? "N/A"; _appState.setIsPaytabsEnabled(Utils.havePrivilege(120)); _appState.setIsPayfortHostedPageEnabled(Utils.havePrivilege(121)); + _appState.setIsMyFatoorahEnabled(Utils.havePrivilege(123)); // Refresh privileges after user switching to ensure correct privileges are loaded if (isSwitchUser) { @@ -1797,6 +1798,7 @@ class AuthenticationViewModel extends ChangeNotifier { _appState.setProjectsDetailList(projectDetailListModel); _appState.setIsPaytabsEnabled(Utils.havePrivilege(120)); _appState.setIsPayfortHostedPageEnabled(Utils.havePrivilege(121)); + _appState.setIsMyFatoorahEnabled(Utils.havePrivilege(123)); } }, ); diff --git a/lib/features/lab/lab_view_model.dart b/lib/features/lab/lab_view_model.dart index 1c25ef9d..d7f5bb62 100644 --- a/lib/features/lab/lab_view_model.dart +++ b/lib/features/lab/lab_view_model.dart @@ -209,12 +209,12 @@ class LabViewModel extends ChangeNotifier { final result = await labRepo.getLabResultReportPDF(labOrder: labOrder); result.fold( - (failure) async => await errorHandlerService.handleError( - failure: failure, - onOkPressed: () { - onError!(failure.message); - }, - ), + // (failure) async => await errorHandlerService.handleError( + (failure) async { + if (onError != null) { + onError(failure.message); + } + }, (apiResponse) { if (apiResponse.messageStatus == 2) { onError!(apiResponse.errorMessage!); diff --git a/lib/features/my_appointments/my_appointments_view_model.dart b/lib/features/my_appointments/my_appointments_view_model.dart index b153c556..b2b990db 100644 --- a/lib/features/my_appointments/my_appointments_view_model.dart +++ b/lib/features/my_appointments/my_appointments_view_model.dart @@ -772,9 +772,9 @@ class MyAppointmentsViewModel extends ChangeNotifier { patientMyDoctorsList = apiResponse.data!; isPatientMyDoctorsLoading = false; - if (!isTop8) { - isMyDoctorsDataToBeLoaded = false; - } + // if (!isTop8) { + isMyDoctorsDataToBeLoaded = false; + // } notifyListeners(); if (onSuccess != null) { diff --git a/lib/features/myfatoorah/myfatoorah_view_model.dart b/lib/features/myfatoorah/myfatoorah_view_model.dart new file mode 100644 index 00000000..b76b251e --- /dev/null +++ b/lib/features/myfatoorah/myfatoorah_view_model.dart @@ -0,0 +1,197 @@ +import 'dart:developer'; + +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_state.dart'; +import 'package:hmg_patient_app_new/core/cache_consts.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/myFatoorah_config.dart'; +import 'package:hmg_patient_app_new/core/utils/utils.dart'; +import 'package:hmg_patient_app_new/features/payfort/models/payfort_request_insert_model.dart'; +import 'package:hmg_patient_app_new/features/payfort/models/payfort_response_insert_model.dart'; +import 'package:hmg_patient_app_new/features/payfort/payfort_repo.dart'; +import 'package:hmg_patient_app_new/generated/codegen_loader.g.dart'; +import 'package:hmg_patient_app_new/services/error_handler_service.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; +import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart'; +import 'package:myfatoorah_flutter/myfatoorah_flutter.dart'; + +class MyFatoorahViewModel extends ChangeNotifier { + final AppState appState; + final ErrorHandlerService errorHandlerService; + final PayfortRepo payfortRepo; + + MyFatoorahViewModel({ + required this.appState, + required this.errorHandlerService, + required this.payfortRepo, + }); + + // Payment Method ID + // 2: Credit/Debit Card + // 6: Mada + + initiateCardPayment({ + num amount = 10.0, + paymentDescription = "", + String displayCurrencyIso = MFCurrencyISO.KUWAIT_KWD, + required String paymentOption, + required String liveServiceID, + required String isScheduleLiveCare, + required String appointmentDate, + required String appointmentNo, + required String doctorID, + required String fileNumber, + required String serviceID, + required String projectID, + Function(MFGetPaymentStatusResponse)? onSuccess, + Function(String)? onError, + }) async { + LoaderBottomSheet.showLoader(); + await MFSDK.init(MyFatoorahConfig.configAPIKey, ApiConsts.myFatoorahCountry, ApiConsts.myFatoorahEnvironmentType); + + MFSDK.setUpActionBar(toolBarTitle: paymentDescription, toolBarTitleColor: "#ffffff", toolBarBackgroundColor: '#ED1C2B', isShowToolBar: true); + + var request = MFExecutePaymentRequest(paymentMethodId: paymentOption.toLowerCase() == "mada" ? 6 : 2, invoiceValue: amount); + request.displayCurrencyIso = displayCurrencyIso; + request.customerName = ApiConsts.appEnvironmentType == AppEnvironmentTypeEnum.uat ? "Test test" : "${appState.getAuthenticatedUser()!.firstName} ${appState.getAuthenticatedUser()!.lastName}"; + request.customerEmail = "CustID_${appState.getAuthenticatedUser()!.patientId}@HMG.com"; + + await MFSDK.executePayment(request, appState.isArabic() ? MFLanguage.ARABIC : MFLanguage.ENGLISH, (invoiceId) async { + PayfortRequestInsertModel payfortRequestInsertModel = PayfortRequestInsertModel(); + + payfortRequestInsertModel.commandType = "SALE"; + payfortRequestInsertModel.amount = amount; + payfortRequestInsertModel.customerEmail = getIt.get().getAuthenticatedUser()!.emailAddress; + payfortRequestInsertModel.orderDescription = paymentDescription; + payfortRequestInsertModel.serviceID = serviceID; + payfortRequestInsertModel.paymentOption = paymentOption; + payfortRequestInsertModel.clientRequestID = invoiceId; + payfortRequestInsertModel.projectID = projectID; + payfortRequestInsertModel.fileNumber = getIt.get().getAuthenticatedUser()!.patientId.toString(); + payfortRequestInsertModel.customerName = "${getIt.get().getAuthenticatedUser()!.firstName} ${getIt.get().getAuthenticatedUser()!.lastName}"; + payfortRequestInsertModel.currency = getIt.get().getAuthenticatedUser()!.outSa! == 0 ? "SAR" : "AED"; + payfortRequestInsertModel.liveServiceID = liveServiceID; + payfortRequestInsertModel.latitude = getIt.get().userLat.toString(); + payfortRequestInsertModel.longitude = getIt.get().userLong.toString(); + payfortRequestInsertModel.merchantReference = invoiceId; + payfortRequestInsertModel.deviceToken = await Utils.getStringFromPrefs(CacheConst.pushToken); + payfortRequestInsertModel.language = getIt.get().isArabic() ? "ar" : "en"; + payfortRequestInsertModel.integrationId = "6"; // PayTabs integration ID + payfortRequestInsertModel.isSchedule = isScheduleLiveCare; + payfortRequestInsertModel.appointmentDate = appointmentDate; + payfortRequestInsertModel.appointmentNo = appointmentNo; + payfortRequestInsertModel.doctorID = doctorID; + + // Call request insert API before payment + await paytabsRequestInsert(payfortRequestInsertModel: payfortRequestInsertModel); + + LoaderBottomSheet.hideLoader(); + }).then((value) async { + var valJson = value.toString(); + print(valJson); + + // // Create PayfortResponseInsertModel (same as Payfort implementation) + PayfortResponseInsertModel payfortResponseInsertModel = PayfortResponseInsertModel(); + + payfortResponseInsertModel.fortId = value.invoiceTransactions!.first.transactionId ?? ""; + payfortResponseInsertModel.commandType = "SALE"; + // payfortResponseInsertModel.amount = paytabsTransactionResponseModel.tranTotal ?? 0; + payfortResponseInsertModel.amount = amount; + payfortResponseInsertModel.sadadOLP = ""; + payfortResponseInsertModel.paymentOption = value.invoiceTransactions!.first.paymentGateway ?? ""; + payfortResponseInsertModel.customerIP = ""; + payfortResponseInsertModel.eCI = ""; + payfortResponseInsertModel.responseMessage = value.invoiceTransactions!.first.transactionStatus ?? ""; + payfortResponseInsertModel.cardNumber = value.invoiceTransactions!.first.cardNumber ?? ""; + payfortResponseInsertModel.cardBin = ""; + payfortResponseInsertModel.installmentsNumber = 0; + payfortResponseInsertModel.status = (value.invoiceStatus ?? "").toLowerCase() == "paid" ? "14" : "00"; + payfortResponseInsertModel.merchantRef = value.invoiceId.toString(); + payfortResponseInsertModel.isRefund = false; + payfortResponseInsertModel.remmeberMe = true; + payfortResponseInsertModel.reconciliationReference = ""; + + // Call response insert API after payment + await paytabsResponseInsert(payfortResponseInsertModel: payfortResponseInsertModel); + + if ((value.invoiceStatus ?? "").toLowerCase() == "paid") { + if (onSuccess != null) { + onSuccess(value); + } + } else { + if (onError != null) { + onError(LocaleKeys.paymentFailedPleaseTryAgain.tr()); + } + } + }).catchError((error) { + if (onError != null) { + onError(error.message); + } + }); + } + + executeApplePayPayment() { + MFApplepay.executeApplePayPayment(onInvoiceCreated: (invoiceID) { + log( + invoiceID.toString(), + ); + }).then((value) { + log( + value.toString(), + ); + }).catchError((error) { + log(error.message); + }); + } + + initApplePayPayment(num invoiceValue) async { + await MFSDK.init(MyFatoorahConfig.configAPIKey, ApiConsts.myFatoorahCountry, ApiConsts.myFatoorahEnvironmentType); + + await MFSDK.initSession(MFInitiateSessionRequest(), MFLanguage.ENGLISH).then((value) async { + var executePaymentRequest = MFExecutePaymentRequest(invoiceValue: invoiceValue, sessionId: value.sessionId, displayCurrencyIso: MFCurrencyISO.SAUDIARABIA_SAR); + + MFApplepay.setupApplePay(value, executePaymentRequest, MFLanguage.ENGLISH, merchantName: "Dr Sulaiman Al Habib"); + }).catchError((error) { + log(error.message); + }); + } + + Future paytabsRequestInsert({required PayfortRequestInsertModel payfortRequestInsertModel, Function(dynamic)? onSuccess, Function(String)? onError}) async { + final result = await payfortRepo.payfortRequestInsert(payfortRequestInsertModel: payfortRequestInsertModel); + + result.fold( + (failure) async => await errorHandlerService.handleError(failure: failure), + (apiResponse) { + if (apiResponse.messageStatus == 2) { + // dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {}); + } else if (apiResponse.messageStatus == 1) { + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse); + } + } + }, + ); + } + + Future paytabsResponseInsert({required PayfortResponseInsertModel payfortResponseInsertModel, Function(dynamic)? onSuccess, Function(String)? onError}) async { + final result = await payfortRepo.payfortResponseInsert(payfortResponseInsertModel: payfortResponseInsertModel); + + result.fold( + (failure) async => await errorHandlerService.handleError(failure: failure), + (apiResponse) { + if (apiResponse.messageStatus == 2) { + // dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {}); + } else if (apiResponse.messageStatus == 1) { + notifyListeners(); + if (onSuccess != null) { + onSuccess(apiResponse); + } + } + }, + ); + } +} diff --git a/lib/features/occasion_theme/occasion_config_model.dart b/lib/features/occasion_theme/occasion_config_model.dart new file mode 100644 index 00000000..03bf9392 --- /dev/null +++ b/lib/features/occasion_theme/occasion_config_model.dart @@ -0,0 +1,123 @@ +import 'package:flutter/material.dart'; + +/// Model representing a remotely-configurable "occasion" theme +/// (e.g. Saudi National Day, Founding Day, Eid, Ramadan, ...). +/// +/// Everything here is fully data-driven — nothing about a specific +/// occasion (name, color, dates, banner asset) is hardcoded in the app. +/// All values are supplied via Firebase Remote Config (or the bundled +/// default JSON fallback used before the first successful fetch). +@immutable +class OccasionConfigModel { + /// Unique identifier of the occasion (e.g. "national_day"). Used only + /// for logging/analytics — no app logic branches on this value. + final String key; + + /// Master kill-switch. When false, the app must look exactly like the + /// default/current theme, regardless of any other field below. + final bool enabled; + + /// Optional activation window. When both are present, the theme is only + /// considered active if `DateTime.now()` falls within [startDate, endDate]. + /// When null, the [enabled] flag alone controls activation. + final DateTime? startDate; + final DateTime? endDate; + + /// Hex string (e.g. "#00894A") for the occasion's primary brand color. + /// When null, the app's default primary color is preserved. + final String? primaryColorHex; + + /// Remote (or bundled asset path) image rendered as a decorative + /// background/watermark, drawn using [backgroundImageOpacity]. + final String? backgroundOpacityImage; + + /// Opacity (0.0 - 1.0) applied to [backgroundOpacityImage]. + final double backgroundImageOpacity; + + /// Remote (or bundled asset path) image rendered as the occasion's main + /// header/banner (e.g. the National Day greeting banner), drawn at full + /// opacity on top of [backgroundOpacityImage]. + final String? headerImage; + + const OccasionConfigModel({ + required this.key, + required this.enabled, + this.startDate, + this.endDate, + this.primaryColorHex, + this.backgroundOpacityImage, + this.backgroundImageOpacity = 1.0, + this.headerImage, + }); + + /// Fully inert config — identical to the app's current/default look. + factory OccasionConfigModel.disabled() => const OccasionConfigModel( + key: 'none', + enabled: false, + startDate: null, + endDate: null, + primaryColorHex: null, + backgroundOpacityImage: null, + backgroundImageOpacity: 1.0, + headerImage: null, + ); + + factory OccasionConfigModel.fromJson(Map json) { + return OccasionConfigModel( + key: (json['key'] as String?) ?? 'none', + enabled: (json['enabled'] as bool?) ?? false, + startDate: _parseDate(json['start_date']), + endDate: _parseDate(json['end_date']), + primaryColorHex: json['primary_color'] as String?, + backgroundOpacityImage: json['background_opacity_image'] as String?, + backgroundImageOpacity: (json['background_image_opacity'] as num?)?.toDouble() ?? 1.0, + headerImage: json['header_image'] as String?, + ); + } + + static DateTime? _parseDate(dynamic value) { + if (value == null) return null; + if (value is String && value.isNotEmpty) { + return DateTime.tryParse(value); + } + return null; + } + + /// Whether "now" falls inside the configured activation window. + /// If no window is configured, the [enabled] flag alone decides. + bool isWithinActiveWindow([DateTime? now]) { + final current = now ?? DateTime.now(); + if (startDate != null && current.isBefore(startDate!)) return false; + if (endDate != null && current.isAfter(endDate!)) return false; + return true; + } + + /// Parsed [Color] from [primaryColorHex], or null if not configured. + Color? get primaryColor => _hexToColor(primaryColorHex); + + /// Lighter/translucent variant of [primaryColor] — 11% opacity of the same + /// hue (e.g. for chip/badge backgrounds). Derived automatically from + /// [primaryColor]; not an independent remote-config field. Null unless + /// [primaryColor] is configured. + Color? get secondaryColor => primaryColor?.withValues(alpha: 0.11); + + static Color? _hexToColor(String? hex) { + if (hex == null || hex.isEmpty) return null; + var value = hex.trim().replaceFirst('#', ''); + if (value.length == 6) value = 'FF$value'; + final parsed = int.tryParse(value, radix: 16); + return parsed != null ? Color(parsed) : null; + } + + Map toJson() => { + 'key': key, + 'enabled': enabled, + 'start_date': startDate?.toIso8601String(), + 'end_date': endDate?.toIso8601String(), + 'primary_color': primaryColorHex, + 'background_opacity_image': backgroundOpacityImage, + 'background_image_opacity': backgroundImageOpacity, + 'header_image': headerImage, + }; +} + diff --git a/lib/features/occasion_theme/occasion_theme_view_model.dart b/lib/features/occasion_theme/occasion_theme_view_model.dart new file mode 100644 index 00000000..9dd22b09 --- /dev/null +++ b/lib/features/occasion_theme/occasion_theme_view_model.dart @@ -0,0 +1,109 @@ +import 'dart:convert'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/painting.dart' show Color; +import 'package:hmg_patient_app_new/features/occasion_theme/occasion_config_model.dart'; +import 'package:hmg_patient_app_new/services/cache_service.dart'; +import 'package:hmg_patient_app_new/services/logger_service.dart'; +import 'package:hmg_patient_app_new/services/remote_config_service.dart'; +import 'package:hmg_patient_app_new/theme/colors.dart'; + +/// Drives the fully remote-config-based "occasion" theming (National Day, +/// Founding Day, Eid, Ramadan, ...). Nothing about a specific occasion is +/// hardcoded here — every color/asset/date comes from [OccasionConfigModel], +/// which is parsed from Firebase Remote Config. +/// +/// Mirrors the existing dark-mode pattern in ProfileSettingsViewModel: +/// a static flag on [AppColors] is kept in sync so every widget using +/// AppColors.primaryRedColor (even ones with no BuildContext/Provider +/// access) reflects the active occasion instantly. +class OccasionThemeViewModel extends ChangeNotifier { + static const String _cachedConfigKey = 'cached_occasion_config_json'; + + final RemoteConfigService _remoteConfigService; + final CacheService _cacheService; + final LoggerService _loggerService; + + OccasionThemeViewModel({ + required RemoteConfigService remoteConfigService, + required CacheService cacheService, + required LoggerService loggerService, + }) : _remoteConfigService = remoteConfigService, + _cacheService = cacheService, + _loggerService = loggerService; + + OccasionConfigModel _config = OccasionConfigModel.disabled(); + OccasionConfigModel get config => _config; + + /// True only when enabled AND (if a window is configured) within it. + bool get isOccasionActive => _config.enabled && _config.isWithinActiveWindow(); + + String? get backgroundOpacityImage => isOccasionActive ? _config.backgroundOpacityImage : null; + + double get backgroundImageOpacity => _config.backgroundImageOpacity; + + /// The occasion's main header/banner image (e.g. National Day greeting + /// banner), rendered at full opacity. Null unless an occasion is active. + String? get headerImage => isOccasionActive ? _config.headerImage : null; + + /// Lighter/translucent variant of the active occasion's primary color + /// (11% opacity) — e.g. for chip/badge backgrounds. Null unless an + /// occasion is active. + Color? get secondaryColor => isOccasionActive ? _config.secondaryColor : null; + + /// Call once at app startup (before the first frame) to restore the last + /// known-good config instantly (offline-safe), then kick off a remote + /// fetch in the background. + Future initialize() async { + _restoreCachedConfig(); + _applyToAppColors(); + + try { + await _remoteConfigService.initialize(); + await refresh(); + } catch (ex) { + _loggerService.logError('OccasionThemeViewModel.initialize error: $ex'); + } + } + + /// Fetches the latest occasion config from Firebase Remote Config and + /// applies it. Safe to call again later (e.g. on app resume) to pick up + /// changes without requiring an app update. + Future refresh() async { + try { + await _remoteConfigService.fetchAndActivate(); + final rawJson = _remoteConfigService.occasionConfigRawJson; + _config = OccasionConfigModel.fromJson(json.decode(rawJson) as Map); + await _cacheService.saveString(key: _cachedConfigKey, value: rawJson); + _applyToAppColors(); + notifyListeners(); + } catch (ex) { + _loggerService.logError('OccasionThemeViewModel.refresh error: $ex'); + // Keep whatever config was already applied (cached/disabled) — never + // let a remote-config failure change the app's appearance unexpectedly. + } + } + + void _restoreCachedConfig() { + try { + final cachedJson = _cacheService.getString(key: _cachedConfigKey); + if (cachedJson == null || cachedJson.isEmpty) { + _config = OccasionConfigModel.disabled(); + return; + } + _config = OccasionConfigModel.fromJson(json.decode(cachedJson) as Map); + } catch (ex) { + _loggerService.logError('OccasionThemeViewModel._restoreCachedConfig error: $ex'); + _config = OccasionConfigModel.disabled(); + } + } + + /// Keeps AppColors in sync so non-Provider call sites (AppColors.primaryRedColor) + /// pick up the occasion color exactly like the existing dark-mode flag does. + void _applyToAppColors() { + AppColors.isOccasionThemeActive = isOccasionActive; + AppColors.occasionPrimaryColor = _config.primaryColor; + AppColors.occasionSecondaryColor = _config.secondaryColor; + } +} + diff --git a/lib/main.dart b/lib/main.dart index 28f86f0d..5e527909 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:io'; import 'package:easy_localization/easy_localization.dart'; @@ -35,7 +36,9 @@ import 'package:hmg_patient_app_new/features/my_appointments/appointment_rating_ 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/features/my_invoices/my_invoices_view_model.dart'; +import 'package:hmg_patient_app_new/features/myfatoorah/myfatoorah_view_model.dart'; import 'package:hmg_patient_app_new/features/notifications/notifications_view_model.dart'; +import 'package:hmg_patient_app_new/features/occasion_theme/occasion_theme_view_model.dart'; import 'package:hmg_patient_app_new/features/offers_and_discounts/offers_and_discounts_view_model.dart'; import 'package:hmg_patient_app_new/features/payfort/payfort_view_model.dart'; import 'package:hmg_patient_app_new/features/paytabs/paytabs_view_model.dart'; @@ -167,6 +170,14 @@ Future callInitializations() async { // Restore persisted accessibility settings before the first frame. getIt.get().loadAccessibilitySettings(); + + // Restore last-known occasion theme instantly (offline-safe, synchronous) + // so the very first frame is already correct, then let the Remote Config + // fetch happen in the background without blocking app startup. Fully + // disabled by default — the app looks exactly as it does today until + // Remote Config enables an occasion (e.g. National Day) on the server side. + final occasionThemeViewModel = getIt.get(); + unawaited(occasionThemeViewModel.initialize()); } void main() async { @@ -304,9 +315,15 @@ void main() async { ChangeNotifierProvider( create: (_) => getIt.get(), ), + ChangeNotifierProvider( + create: (_) => getIt.get(), + ), ChangeNotifierProvider( create: (_) => getIt.get(), - ) + ), + ChangeNotifierProvider( + create: (_) => getIt.get(), + ), ], child: MyApp()), ), // Wrap your app ); @@ -374,11 +391,11 @@ class MyApp extends StatelessWidget { return Sizer( builder: (context, orientation, deviceType) { // SizeConfig().init(constraints, orientation); - return Consumer( - builder: (context, profileVm, _) { + return Consumer2( + builder: (context, profileVm, occasionThemeVm, _) { final isArabic = EasyLocalization.of(context)?.locale.languageCode == "ar"; return MaterialApp( - key: ValueKey('app_theme_${profileVm.isDarkMode}'), + key: ValueKey('app_theme_${profileVm.isDarkMode}_${occasionThemeVm.isOccasionActive}_${occasionThemeVm.config.key}'), // title: 'Dr. AlHabib', title: 'Dr. AlHabib Beta', builder: (context, mchild) { diff --git a/lib/presentation/appointments/appointment_details_page.dart b/lib/presentation/appointments/appointment_details_page.dart index dc147981..7d1d3f91 100644 --- a/lib/presentation/appointments/appointment_details_page.dart +++ b/lib/presentation/appointments/appointment_details_page.dart @@ -989,7 +989,7 @@ class _AppointmentDetailsPageState extends State { openDoctorScheduleCalendar(); }, backgroundColor: AppColors.primaryRedColor, - borderColor: AppColors.primaryRedColor, + borderColor: Colors.transparent, textColor: Colors.white, fontSize: 16.f, fontWeight: FontWeight.w600, @@ -1097,7 +1097,7 @@ class _AppointmentDetailsPageState extends State { openDoctorScheduleCalendar(); }, backgroundColor: AppColors.primaryRedColor, - borderColor: AppColors.primaryRedColor, + borderColor: Colors.transparent, textColor: Colors.white, fontSize: 16.f, fontWeight: FontWeight.w600, diff --git a/lib/presentation/appointments/my_appointments_page.dart b/lib/presentation/appointments/my_appointments_page.dart index da20e183..e15edb47 100644 --- a/lib/presentation/appointments/my_appointments_page.dart +++ b/lib/presentation/appointments/my_appointments_page.dart @@ -67,8 +67,8 @@ class _MyAppointmentsPageState extends State { SizedBox(height: 16.h), CustomTabBar( initialIndex: myAppointmentsVM.selectedTabIndex, - activeTextColor: Color(0xffED1C2B), - activeBackgroundColor: Color(0xffED1C2B).withValues(alpha: .1), + activeTextColor: AppColors.primaryRedColor, + activeBackgroundColor: AppColors.secondaryLightRedColor, tabs: [ // CustomTabBarModel(null, LocaleKeys.allAppt.tr(context: context)), CustomTabBarModel(null, LocaleKeys.upcoming.tr(context: context)), @@ -269,9 +269,9 @@ class _MyAppointmentsPageState extends State { ), ); }, - backgroundColor: Color(0xffFEE9EA), - borderColor: Color(0xffFEE9EA), - textColor: Color(0xffED1C2B), + backgroundColor: AppColors.secondaryLightRedColor, + borderColor: AppColors.secondaryLightRedColor, + textColor: AppColors.primaryRedColor, fontSize: 14.f, isBold: true, borderRadius: 12.r, diff --git a/lib/presentation/appointments/widgets/appointment_doctor_card.dart b/lib/presentation/appointments/widgets/appointment_doctor_card.dart index 4dd838ea..a3b13259 100644 --- a/lib/presentation/appointments/widgets/appointment_doctor_card.dart +++ b/lib/presentation/appointments/widgets/appointment_doctor_card.dart @@ -84,8 +84,7 @@ class AppointmentDoctorCard extends StatelessWidget { children: [ Utils.buildSvgWithAssets(icon: AppAssets.rating_icon, width: 15.h, height: 15.h, iconColor: AppColors.ratingColorYellow), SizedBox(height: 2.h), - "${patientAppointmentHistoryResponseModel.decimalDoctorRate ?? 0.0}" - .toText11(isBold: true, color: AppColors.textColor, isEnglishOnly: true), + "${patientAppointmentHistoryResponseModel.decimalDoctorRate ?? 0.0}".toText11(isBold: true, color: AppColors.textColor, isEnglishOnly: true), ], ), ).circle(100), @@ -102,11 +101,7 @@ class AppointmentDoctorCard extends StatelessWidget { children: [ Expanded( child: (patientAppointmentHistoryResponseModel.doctorNameObj ?? "").toText16( - isBold: true, - isEnglishOnly: !Utils.isArabicText(patientAppointmentHistoryResponseModel.doctorNameObj ?? ""), - textOverflow: TextOverflow.ellipsis, - maxlines: 2 - ), + isBold: true, isEnglishOnly: !Utils.isArabicText(patientAppointmentHistoryResponseModel.doctorNameObj ?? ""), textOverflow: TextOverflow.ellipsis, maxlines: 2), ), SizedBox(width: 12.w), // (patientAppointmentHistoryResponseModel.doctorNationalityFlagURL != null && @@ -142,18 +137,17 @@ class AppointmentDoctorCard extends StatelessWidget { child: AppCustomChipWidget( labelPadding: EdgeInsetsDirectional.only(start: -6.w, end: 6.w), icon: AppAssets.doctor_calendar_icon, - richText: "${DateUtil.formatDateToDate(DateUtil.convertStringToDate(patientAppointmentHistoryResponseModel.appointmentDate), false)} ${(patientAppointmentHistoryResponseModel.startTime != null && patientAppointmentHistoryResponseModel.startTime!.isNotEmpty) ? patientAppointmentHistoryResponseModel.startTime!.substring(0, 5) : DateUtil.formatDateToTimeLang(DateUtil.convertStringToDateSaudiTimezone(patientAppointmentHistoryResponseModel.appointmentDate!, patientAppointmentHistoryResponseModel.projectID), false)}" - .toText10(isBold: true), + richText: + "${DateUtil.formatDateToDate(DateUtil.convertStringToDate(patientAppointmentHistoryResponseModel.appointmentDate), false)} ${(patientAppointmentHistoryResponseModel.startTime != null && patientAppointmentHistoryResponseModel.startTime!.isNotEmpty) ? patientAppointmentHistoryResponseModel.startTime!.substring(0, 5) : DateUtil.formatDateToTimeLang(DateUtil.convertStringToDateSaudiTimezone(patientAppointmentHistoryResponseModel.appointmentDate!, patientAppointmentHistoryResponseModel.projectID), false)}" + .toText10(isBold: true), ), ), ), AppCustomChipWidget( labelPadding: EdgeInsetsDirectional.only(start: -6.w, end: 6.w), - icon: !(patientAppointmentHistoryResponseModel.isLiveCareAppointment ?? false) ? AppAssets.walkin_appointment_icon - : AppAssets.small_livecare_icon, + icon: !(patientAppointmentHistoryResponseModel.isLiveCareAppointment ?? false) ? AppAssets.walkin_appointment_icon : AppAssets.small_livecare_icon, iconColor: !(patientAppointmentHistoryResponseModel.isLiveCareAppointment ?? false) ? AppColors.textColor : Colors.white, - labelText: (patientAppointmentHistoryResponseModel.isLiveCareAppointment ?? false) ? LocaleKeys.livecare.tr(context: context) - : LocaleKeys.walkin.tr(context: context), + labelText: (patientAppointmentHistoryResponseModel.isLiveCareAppointment ?? false) ? LocaleKeys.livecare.tr(context: context) : LocaleKeys.walkin.tr(context: context), backgroundColor: !(patientAppointmentHistoryResponseModel.isLiveCareAppointment ?? false) ? AppColors.greyColor : AppColors.successColor, textColor: !(patientAppointmentHistoryResponseModel.isLiveCareAppointment ?? false) ? AppColors.textColor : Colors.white, ), @@ -163,13 +157,12 @@ class AppointmentDoctorCard extends StatelessWidget { ), ), (patientAppointmentHistoryResponseModel.isLiveCareAppointment! || - patientAppointmentHistoryResponseModel.isExecludeDoctor! || + patientAppointmentHistoryResponseModel.isExecludeDoctor! || patientAppointmentHistoryResponseModel.isClinicReBookingAllowed! == false || patientAppointmentHistoryResponseModel.isActiveDoctor! == false || - !Utils.isClinicAllowedForRebook(patientAppointmentHistoryResponseModel.clinicID)) + !Utils.isClinicAllowedForRebook(patientAppointmentHistoryResponseModel.clinicID)) ? SizedBox.shrink() - : Utils.buildSvgWithAssets(icon: AppAssets.doctor_profile_icon, width: 20.h, height: 20.h, fit: BoxFit.scaleDown) - .onPress(() async { + : 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; @@ -270,7 +263,7 @@ class AppointmentDoctorCard extends StatelessWidget { onAskDoctorTap(); }, backgroundColor: AppColors.secondaryLightRedColor, - borderColor: AppColors.secondaryLightRedColor, + borderColor: Colors.transparent, textColor: AppColors.primaryRedColor, fontSize: 14.f, isBold: true, @@ -310,7 +303,7 @@ class AppointmentDoctorCard extends StatelessWidget { onCancelTap(); }, backgroundColor: AppColors.primaryRedColor, - borderColor: AppColors.primaryRedColor, + borderColor: Colors.transparent, textColor: AppColors.whiteColor, fontSize: 14.f, isBold: true, @@ -329,7 +322,7 @@ class AppointmentDoctorCard extends StatelessWidget { onRescheduleTap(); }, backgroundColor: AppColors.secondaryLightRedColor, - borderColor: AppColors.secondaryLightRedColor, + borderColor: Colors.transparent, textColor: AppColors.primaryRedColor, fontSize: 14.f, isBold: true, diff --git a/lib/presentation/appointments/widgets/appointment_rating_widget.dart b/lib/presentation/appointments/widgets/appointment_rating_widget.dart index 9b13c6cd..ae70da1d 100644 --- a/lib/presentation/appointments/widgets/appointment_rating_widget.dart +++ b/lib/presentation/appointments/widgets/appointment_rating_widget.dart @@ -142,9 +142,9 @@ class _AppointmentRatingWidgetState extends State { Expanded( child: CustomButton( text: LocaleKeys.later.tr(context: context), - backgroundColor: Color(0xffFEE9EA), - borderColor: Color(0xffFEE9EA), - textColor: Color(0xffED1C2B), + backgroundColor: AppColors.secondaryLightRedColor, + borderColor: AppColors.secondaryLightRedColor, + textColor: AppColors.primaryRedColor, onPressed: () { Navigator.pop(context); }, diff --git a/lib/presentation/authentication/quick_login.dart b/lib/presentation/authentication/quick_login.dart index 133e38ef..00e872a3 100644 --- a/lib/presentation/authentication/quick_login.dart +++ b/lib/presentation/authentication/quick_login.dart @@ -94,17 +94,18 @@ class QuickLoginState extends State { mainAxisAlignment: MainAxisAlignment.end, children: [ Expanded( - child: CustomButton( - text: LocaleKeys.enableQuickLoginBtn.tr(context: context), - onPressed: () { - widget.onPressed(); - }, - backgroundColor: Color(0xffED1C2B), - borderColor: Color(0xffED1C2B), - textColor: Colors.white, - icon: AppAssets.apple_finder, - height: 56.h, - )), + child: CustomButton( + text: LocaleKeys.enableQuickLoginBtn.tr(context: context), + onPressed: () { + widget.onPressed(); + }, + backgroundColor: AppColors.primaryRedColor, + borderColor: AppColors.primaryRedColor, + textColor: Colors.white, + icon: AppAssets.apple_finder, + height: 56.h, + ), + ), ], ), SizedBox( @@ -120,9 +121,9 @@ class QuickLoginState extends State { cacheService.saveBool(key: CacheConst.quickLoginEnabled, value: false); Navigator.pop(context, "true"); }, - backgroundColor: Color(0xffFEE9EA), - borderColor: Color(0xffFEE9EA), - textColor: Colors.red, + backgroundColor: AppColors.secondaryLightRedColor, + borderColor: Colors.transparent, + textColor: AppColors.primaryRedColor, height: 56.h, // icon: "assets/images/svg/apple-finder.svg", )), diff --git a/lib/presentation/authentication/saved_login_screen.dart b/lib/presentation/authentication/saved_login_screen.dart index bea93d36..4de4bb64 100644 --- a/lib/presentation/authentication/saved_login_screen.dart +++ b/lib/presentation/authentication/saved_login_screen.dart @@ -327,9 +327,9 @@ class _SavedLogin extends State with RouteAware { // MaterialPageRoute(builder: (BuildContext context) => LandingNavigation()) // ); }, - backgroundColor: Color(0xffFEE9EA), - borderColor: Color(0xffFEE9EA), - textColor: Color(0xffED1C2B), + backgroundColor: AppColors.secondaryLightRedColor, + borderColor: Colors.transparent, + textColor: AppColors.primaryRedColor, fontSize: 16.f, fontWeight: FontWeight.w600, borderRadius: 12.r, @@ -352,9 +352,9 @@ class _SavedLogin extends State with RouteAware { MaterialPageRoute(builder: (BuildContext context) => LoginScreen()), ); }, - backgroundColor: Color(0xffFEE9EA), - borderColor: Color(0xffFEE9EA), - textColor: Color(0xffED1C2B), + backgroundColor: AppColors.secondaryLightRedColor, + borderColor: Colors.transparent, + textColor: AppColors.primaryRedColor, fontSize: 15.f, fontWeight: FontWeight.w600, borderRadius: 12.r, diff --git a/lib/presentation/blood_donation/blood_donation_page.dart b/lib/presentation/blood_donation/blood_donation_page.dart index f3f0782d..b4bdb6df 100644 --- a/lib/presentation/blood_donation/blood_donation_page.dart +++ b/lib/presentation/blood_donation/blood_donation_page.dart @@ -232,7 +232,7 @@ class _BloodDonationPageState extends State { Container( padding: EdgeInsets.all(16.h), decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.bgRedLightColor.withValues(alpha: 0.1), + color: AppColors.secondaryLightRedColorBase.withValues(alpha: 0.1), borderRadius: 12.r, hasShadow: false, ), diff --git a/lib/presentation/book_appointment/book_appointment_page.dart b/lib/presentation/book_appointment/book_appointment_page.dart index c2560ecd..57b3d541 100644 --- a/lib/presentation/book_appointment/book_appointment_page.dart +++ b/lib/presentation/book_appointment/book_appointment_page.dart @@ -106,8 +106,8 @@ class _BookAppointmentPageState extends State { children: [ SizedBox(height: 16.h), CustomTabBar( - activeTextColor: Color(0xffED1C2B), - activeBackgroundColor: Color(0xffED1C2B).withValues(alpha: .1), + activeTextColor: AppColors.primaryRedColor, + activeBackgroundColor: AppColors.secondaryLightRedColor, initialIndex: bookAppointmentsVM.selectedTabIndex, tabs: [ CustomTabBarModel(null, LocaleKeys.general.tr(context: context)), @@ -326,9 +326,9 @@ class _BookAppointmentPageState extends State { onPressed: () { Navigator.of(context).push(CustomPageRoute(page: SearchDoctorByName())); }, - backgroundColor: Color(0xffFEE9EA), - borderColor: Color(0xffFEE9EA), - textColor: Color(0xffED1C2B), + backgroundColor: AppColors.secondaryLightRedColor, + borderColor: AppColors.secondaryLightRedColor, + textColor: AppColors.primaryRedColor, fontSize: 14.f, isBold: true, padding: EdgeInsets.fromLTRB(10.h, 0, 10.h, 0), diff --git a/lib/presentation/book_appointment/doctor_profile_page.dart b/lib/presentation/book_appointment/doctor_profile_page.dart index 5b6ad09d..ee4d859b 100644 --- a/lib/presentation/book_appointment/doctor_profile_page.dart +++ b/lib/presentation/book_appointment/doctor_profile_page.dart @@ -60,9 +60,7 @@ class DoctorProfilePage extends StatelessWidget { doctorID: viewModel.doctorsProfileResponseModel.doctorID ?? 0, isActive: viewModel.isFavouriteDoctor, onSuccess: (response) { - Utils.showToast(viewModel.isFavouriteDoctor - ? LocaleKeys.doctorAddedToFavourite.tr(context: context) - : LocaleKeys.doctorRemovedFromFavourite.tr(context: context)); + Utils.showToast(viewModel.isFavouriteDoctor ? LocaleKeys.doctorAddedToFavourite.tr(context: context) : LocaleKeys.doctorRemovedFromFavourite.tr(context: context)); // Successfully added/removed favorite - refresh the favorites list getIt.get().refreshFavouriteDoctors(); }, @@ -101,14 +99,11 @@ class DoctorProfilePage extends StatelessWidget { children: [ SizedBox( width: isFoldable ? 250.w : 220.w, - child: - ("${bookAppointmentsViewModel.doctorsProfileResponseModel.doctorTitleForProfile} ${bookAppointmentsViewModel.doctorsProfileResponseModel.doctorName}") - .toString() - .toText24(isBold: true), + child: ("${bookAppointmentsViewModel.doctorsProfileResponseModel.doctorTitleForProfile} ${bookAppointmentsViewModel.doctorsProfileResponseModel.doctorName}") + .toString() + .toText24(isBold: true), ), - (bookAppointmentsViewModel.doctorsProfileResponseModel.specialty!.isNotEmpty - ? bookAppointmentsViewModel.doctorsProfileResponseModel.specialty!.first - : "") + (bookAppointmentsViewModel.doctorsProfileResponseModel.specialty!.isNotEmpty ? bookAppointmentsViewModel.doctorsProfileResponseModel.specialty!.first : "") .toString() .toText18(isBold: true, color: AppColors.primaryRedColor), ], @@ -148,16 +143,12 @@ class DoctorProfilePage extends StatelessWidget { children: [ Column( children: [ - Utils.buildSvgWithAssets( - icon: AppAssets.doctor_profile_rating_icon, width: 48.w, height: 48.h, fit: BoxFit.contain, applyThemeColor: false), + Utils.buildSvgWithAssets(icon: AppAssets.doctor_profile_rating_icon, width: 48.w, height: 48.h, fit: BoxFit.contain, applyThemeColor: false), SizedBox(height: 16.h), LocaleKeys.ratings.tr(context: context).toText12(isBold: true, color: AppColors.greyTextColor), - bookAppointmentsViewModel.doctorsProfileResponseModel.decimalDoctorRate.toString().toText16( - isBold: true, - color: AppColors.textColor, - isUnderLine: true, - decorationColor: AppColors.textColor, - fontFamily: "Poppins"), + bookAppointmentsViewModel.doctorsProfileResponseModel.decimalDoctorRate + .toString() + .toText16(isBold: true, color: AppColors.textColor, isUnderLine: true, decorationColor: AppColors.textColor, fontFamily: "Poppins"), ], ).onPress(() { bookAppointmentsViewModel.getDoctorRatingDetails(); @@ -173,18 +164,12 @@ class DoctorProfilePage extends StatelessWidget { SizedBox(width: 36.w), Column( children: [ - Utils.buildSvgWithAssets( - icon: AppAssets.doctor_profile_reviews_icon, width: 48.w, height: 48.h, fit: BoxFit.contain, applyThemeColor: false), + 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(isBold: true, color: AppColors.greyTextColor), NumberFormat.decimalPattern("en_US") .format(bookAppointmentsViewModel.doctorsProfileResponseModel.noOfPatientsRate ?? 0) - .toText16( - isBold: true, - color: AppColors.textColor, - isUnderLine: true, - decorationColor: AppColors.textColor, - fontFamily: "Poppins"), + .toText16(isBold: true, color: AppColors.textColor, isUnderLine: true, decorationColor: AppColors.textColor, fontFamily: "Poppins"), ], ).onPress(() { bookAppointmentsViewModel.getDoctorRatingDetails(); @@ -215,9 +200,7 @@ class DoctorProfilePage extends StatelessWidget { SizedBox(height: 16.h), LocaleKeys.information.tr(context: context).toText14(isBold: true, color: AppColors.textColor), SizedBox(height: 6.h), - (bookAppointmentsViewModel.doctorsProfileResponseModel.doctorProfileInfo ?? "") - .trim() - .toText12(isBold: true, color: AppColors.greyTextColor), + (bookAppointmentsViewModel.doctorsProfileResponseModel.doctorProfileInfo ?? "").trim().toText12(isBold: true, color: AppColors.greyTextColor), SizedBox(height: 24.h), ], ).paddingSymmetrical(24.h, 0.h), @@ -238,8 +221,7 @@ class DoctorProfilePage extends StatelessWidget { bookAppointmentsViewModel.selectedDoctor.specialityN = bookAppointmentsViewModel.doctorsProfileResponseModel.specialty; bookAppointmentsViewModel.selectedDoctor.name = bookAppointmentsViewModel.doctorsProfileResponseModel.doctorName; bookAppointmentsViewModel.selectedDoctor.doctorImageURL = bookAppointmentsViewModel.doctorsProfileResponseModel.doctorImageURL; - bookAppointmentsViewModel.selectedDoctor.nationalityFlagURL = - bookAppointmentsViewModel.doctorsProfileResponseModel.nationalityFlagURL; + bookAppointmentsViewModel.selectedDoctor.nationalityFlagURL = bookAppointmentsViewModel.doctorsProfileResponseModel.nationalityFlagURL; bookAppointmentsViewModel.selectedDoctor.clinicName = bookAppointmentsViewModel.doctorsProfileResponseModel.clinicDescription; bookAppointmentsViewModel.selectedDoctor.projectName = bookAppointmentsViewModel.doctorsProfileResponseModel.projectName; diff --git a/lib/presentation/book_appointment/laser/laser_appointment.dart b/lib/presentation/book_appointment/laser/laser_appointment.dart index c8773933..affd5423 100644 --- a/lib/presentation/book_appointment/laser/laser_appointment.dart +++ b/lib/presentation/book_appointment/laser/laser_appointment.dart @@ -80,8 +80,8 @@ class LaserAppointment extends StatelessWidget { child: Column( children: [ CustomTabBar( - activeTextColor: Color(0xffED1C2B), - activeBackgroundColor: Color(0xffED1C2B).withValues(alpha: .1), + activeTextColor: AppColors.primaryRedColor, + activeBackgroundColor: AppColors.secondaryLightRedColor, tabs: [ CustomTabBarModel(null, LocaleKeys.malE.tr()), CustomTabBarModel(null, LocaleKeys.femaleGender.tr()), diff --git a/lib/presentation/book_appointment/livecare/immediate_livecare_payment_page.dart b/lib/presentation/book_appointment/livecare/immediate_livecare_payment_page.dart index 31defa56..2147cbd6 100644 --- a/lib/presentation/book_appointment/livecare/immediate_livecare_payment_page.dart +++ b/lib/presentation/book_appointment/livecare/immediate_livecare_payment_page.dart @@ -11,8 +11,10 @@ import 'package:hmg_patient_app_new/core/app_state.dart'; import 'package:hmg_patient_app_new/core/cache_consts.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/myFatoorah_config.dart'; import 'package:hmg_patient_app_new/core/utils/date_util.dart'; import 'package:hmg_patient_app_new/features/immediate_livecare/immediate_livecare_view_model.dart'; +import 'package:hmg_patient_app_new/features/myfatoorah/myfatoorah_view_model.dart'; import 'package:hmg_patient_app_new/features/payfort/models/apple_pay_request_insert_model.dart'; import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; import 'package:hmg_patient_app_new/core/utils/utils.dart'; @@ -33,6 +35,8 @@ import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart'; import 'package:hmg_patient_app_new/widgets/in_app_browser/InAppBrowser.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:myfatoorah_flutter/MFEnums.dart'; +import 'package:myfatoorah_flutter/myfatoorah_flutter.dart'; import 'package:provider/provider.dart'; import 'package:smooth_corner/smooth_corner.dart'; @@ -49,6 +53,9 @@ class _ImmediateLiveCarePaymentPageState extends State= myAppointmentsViewModel.getTamaraInstallmentsDetailsResponseModel!.minLimit!.amount! && @@ -80,11 +92,15 @@ class _ImmediateLiveCarePaymentPageState extends State(); myAppointmentsViewModel = Provider.of(context, listen: false); immediateLiveCareViewModel = Provider.of(context, listen: false); payfortViewModel = Provider.of(context, listen: false); paytabsViewModel = Provider.of(context, listen: false); + myFatoorahViewModel = Provider.of(context, listen: false); return Scaffold( backgroundColor: AppColors.bgScaffoldColor, body: Consumer(builder: (context, myAppointmentsVM, child) { @@ -131,7 +147,62 @@ class _ImmediateLiveCarePaymentPageState extends State false); + Navigator.of(context).push( + CustomPageRoute( + page: ImmediateLiveCarePendingRequestPage(), + ), + ); + } else { + LoaderBottomSheet.hideLoader(); + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: "Unknown error occurred..."), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + } + }, + onError: (errMessage) { + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: errMessage.toString()), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + }); + } else if (appState.isPaytabsEnabled) { transID = Utils.getAppointmentTransID( immediateLiveCareViewModel.immediateLiveCareSelectedClinic.serviceID!, ApiConsts.appEnvironmentType == AppEnvironmentTypeEnum.uat ? 15 : 12, @@ -298,7 +369,62 @@ class _ImmediateLiveCarePaymentPageState extends State false); + Navigator.of(context).push( + CustomPageRoute( + page: ImmediateLiveCarePendingRequestPage(), + ), + ); + } else { + LoaderBottomSheet.hideLoader(); + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: "Unknown error occurred..."), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + } + }, + onError: (errMessage) { + showCommonBottomSheetWithoutHeight( + context, + child: Utils.getErrorWidget(loadingText: errMessage.toString()), + callBackFunc: () {}, + isFullScreen: false, + isCloseButtonVisible: true, + ); + }); + } else if (appState.isPaytabsEnabled) { transID = Utils.getAppointmentTransID( immediateLiveCareViewModel.immediateLiveCareSelectedClinic.serviceID!, ApiConsts.appEnvironmentType == AppEnvironmentTypeEnum.uat ? 15 : 12, @@ -610,16 +736,31 @@ class _ImmediateLiveCarePaymentPageState extends State 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 3d98e5a1..026c1725 100644 --- a/lib/presentation/home/data/landing_page_data.dart +++ b/lib/presentation/home/data/landing_page_data.dart @@ -9,192 +9,190 @@ import 'package:hmg_patient_app_new/theme/colors.dart'; class LandingPageData { static List get getNotLoggedInServiceCardsList => [ - ServiceCardData( - serviceName: "emergency", - icon: AppAssets.emergency_services_icon, - title: getIt.get().isArabic()?LocaleKeys.services3 :LocaleKeys.emergency2, - subtitle: getIt.get().isArabic()?LocaleKeys.emergency2:LocaleKeys.services3, - backgroundColor: AppColors.primaryRedColor, - iconColor: Colors.white, - textColor: Colors.white, - isBold: true, - ), - ServiceCardData( - serviceName: "search_doctor", - icon: AppAssets.search_doctor_icon, - title: LocaleKeys.finda, - subtitle: LocaleKeys.doctor, - backgroundColor: AppColors.whiteColor, - iconColor: AppColors.blackColor, - textColor: AppColors.blackColor, - isBold: false, - ), - ServiceCardData( - serviceName: "health_calculators_and_converts", - icon: AppAssets.health_calculators_icon, - title: getIt.get().isArabic()? LocaleKeys.calculators:LocaleKeys.health , - subtitle: getIt.get().isArabic()? LocaleKeys.health:LocaleKeys.calculators, - backgroundColor: AppColors.whiteColor, - iconColor: AppColors.blackColor, - textColor: AppColors.blackColor, - isBold: false, - ), - ServiceCardData( - serviceName: "health_converters", - icon: AppAssets.health_converters_icon, - title: getIt.get().isArabic()? LocaleKeys.converters:LocaleKeys.health, - subtitle: getIt.get().isArabic()? LocaleKeys.health:LocaleKeys.converters, - backgroundColor: AppColors.whiteColor, - iconColor: AppColors.blackColor, - textColor: AppColors.blackColor, - isBold: false, - ), - // ServiceCardData( - // serviceName: "parking_guide", - // icon: AppAssets.car_parking_icon, - // title: getIt.get().isArabic()? LocaleKeys.guide:LocaleKeys.parking2, - // subtitle: getIt.get().isArabic()? LocaleKeys.parking2:LocaleKeys.guide, - // backgroundColor: AppColors.whiteColor, - // iconColor: AppColors.blackColor, - // textColor: AppColors.blackColor, - // isBold: false, - // ), - ServiceCardData( - serviceName: "indoor_navigation", - icon: AppAssets.indoor_nav_icon, - title: LocaleKeys.indoor, - subtitle: LocaleKeys.thehospital, - backgroundColor: AppColors.whiteColor, - iconColor: AppColors.blackColor, - textColor: AppColors.blackColor, - isBold: false, - ), - ]; + ServiceCardData( + serviceName: "emergency", + icon: AppAssets.emergency_services_icon, + title: getIt.get().isArabic() ? LocaleKeys.services3 : LocaleKeys.emergency2, + subtitle: getIt.get().isArabic() ? LocaleKeys.emergency2 : LocaleKeys.services3, + backgroundColor: AppColors.primaryRedColorBase, + iconColor: Colors.white, + textColor: Colors.white, + isBold: true, + ), + ServiceCardData( + serviceName: "search_doctor", + icon: AppAssets.search_doctor_icon, + title: LocaleKeys.finda, + subtitle: LocaleKeys.doctor, + backgroundColor: AppColors.whiteColor, + iconColor: AppColors.blackColor, + textColor: AppColors.blackColor, + isBold: false, + ), + ServiceCardData( + serviceName: "health_calculators_and_converts", + icon: AppAssets.health_calculators_icon, + title: getIt.get().isArabic() ? LocaleKeys.calculators : LocaleKeys.health, + subtitle: getIt.get().isArabic() ? LocaleKeys.health : LocaleKeys.calculators, + backgroundColor: AppColors.whiteColor, + iconColor: AppColors.blackColor, + textColor: AppColors.blackColor, + isBold: false, + ), + ServiceCardData( + serviceName: "health_converters", + icon: AppAssets.health_converters_icon, + title: getIt.get().isArabic() ? LocaleKeys.converters : LocaleKeys.health, + subtitle: getIt.get().isArabic() ? LocaleKeys.health : LocaleKeys.converters, + backgroundColor: AppColors.whiteColor, + iconColor: AppColors.blackColor, + textColor: AppColors.blackColor, + isBold: false, + ), + // ServiceCardData( + // serviceName: "parking_guide", + // icon: AppAssets.car_parking_icon, + // title: getIt.get().isArabic()? LocaleKeys.guide:LocaleKeys.parking2, + // subtitle: getIt.get().isArabic()? LocaleKeys.parking2:LocaleKeys.guide, + // backgroundColor: AppColors.whiteColor, + // iconColor: AppColors.blackColor, + // textColor: AppColors.blackColor, + // isBold: false, + // ), + ServiceCardData( + serviceName: "indoor_navigation", + icon: AppAssets.indoor_nav_icon, + title: LocaleKeys.indoor, + subtitle: LocaleKeys.thehospital, + backgroundColor: AppColors.whiteColor, + iconColor: AppColors.blackColor, + textColor: AppColors.blackColor, + isBold: false, + ), + ]; List get getLoggedInServiceCardsList => [ - ServiceCardData( - serviceName: "emergency", - icon: AppAssets.emergency_services_icon, - title: getIt.get().isArabic()?LocaleKeys.services3 :LocaleKeys.emergency2, - subtitle: getIt.get().isArabic()?LocaleKeys.emergency2:LocaleKeys.services3, - backgroundColor: AppColors.primaryRedColor, - iconColor: Colors.white, - textColor: Colors.white, - isBold: true, - ), - ServiceCardData( - serviceName: "lab_results", - icon: AppAssets.home_lab_result_icon, - title: getIt.get().isArabic()?LocaleKeys.labSubtitle:LocaleKeys.lab, - subtitle: getIt.get().isArabic()?LocaleKeys.lab:LocaleKeys.labSubtitle , - backgroundColor: AppColors.whiteColor, - iconColor: AppColors.blackColor, - textColor: AppColors.blackColor, - isBold: false, - ), - ServiceCardData( - serviceName: "radiology_results", - icon: AppAssets.my_radiology_icon, - title: getIt.get().isArabic()?LocaleKeys.labSubtitle:LocaleKeys.radiology, - subtitle: getIt.get().isArabic()?LocaleKeys.radiology:LocaleKeys.labSubtitle , - backgroundColor: AppColors.whiteColor, - iconColor: AppColors.blackColor, - textColor: AppColors.blackColor, - isBold: false, - ), - ServiceCardData( - serviceName: "prescriptions", - icon: AppAssets.my_prescription_icon, - title: getIt.get().isArabic()?LocaleKeys.medicinesSubtitle:LocaleKeys.medicalKey, - subtitle: getIt.get().isArabic()?LocaleKeys.medicalKey:LocaleKeys.medicinesSubtitle, - backgroundColor: AppColors.whiteColor, - iconColor: AppColors.blackColor, - textColor: AppColors.blackColor, - isBold: false, - ), - ServiceCardData( - serviceName: "insurance_update", - icon: AppAssets.insurance_update_icon, - title: getIt.get().isArabic()?LocaleKeys.insurance :LocaleKeys.insurance, - subtitle: getIt.get().isArabic()?LocaleKeys.medical: LocaleKeys.details2, - backgroundColor: AppColors.whiteColor, - iconColor: AppColors.blackColor, - textColor: AppColors.blackColor, - isBold: false, - ), - ServiceCardData( - serviceName: "my_doctors", - icon: AppAssets.my_doctors_icon, - title: getIt.get().isArabic() ? LocaleKeys.myDoctorSubtitle2.tr() : "My", - subtitle: getIt.get().isArabic() ? LocaleKeys.myDoctor.tr() : "Doctors", - backgroundColor: AppColors.whiteColor, - iconColor: AppColors.blackColor, - textColor: AppColors.blackColor, - isBold: false, - ), - ServiceCardData( - serviceName: "sick_leaves", - icon: AppAssets.my_sick_leave_icon, - title: getIt.get().isArabic()?LocaleKeys.sickSubtitle:LocaleKeys.sick2, - subtitle: getIt.get().isArabic()?LocaleKeys.sick2:LocaleKeys.sickSubtitle, - backgroundColor: AppColors.whiteColor, - iconColor: AppColors.blackColor, - textColor: AppColors.blackColor, - isBold: false, - ), - ServiceCardData( - serviceName: "my_invoice", - icon: AppAssets.invoiceListIcon, - title: getIt.get().isArabic()?LocaleKeys.myDoctorSubtitle2:LocaleKeys.invoiceList2, - subtitle: getIt.get().isArabic()?LocaleKeys.invoiceList2:LocaleKeys.myDoctorSubtitle2, - backgroundColor: AppColors.whiteColor, - iconColor: AppColors.blackColor, - textColor: AppColors.blackColor, - isBold: false, - ), - ]; + ServiceCardData( + serviceName: "emergency", + icon: AppAssets.emergency_services_icon, + title: getIt.get().isArabic() ? LocaleKeys.services3 : LocaleKeys.emergency2, + subtitle: getIt.get().isArabic() ? LocaleKeys.emergency2 : LocaleKeys.services3, + backgroundColor: AppColors.primaryRedColorBase, + iconColor: Colors.white, + textColor: Colors.white, + isBold: true, + ), + ServiceCardData( + serviceName: "lab_results", + icon: AppAssets.home_lab_result_icon, + title: getIt.get().isArabic() ? LocaleKeys.labSubtitle : LocaleKeys.lab, + subtitle: getIt.get().isArabic() ? LocaleKeys.lab : LocaleKeys.labSubtitle, + backgroundColor: AppColors.whiteColor, + iconColor: AppColors.blackColor, + textColor: AppColors.blackColor, + isBold: false, + ), + ServiceCardData( + serviceName: "radiology_results", + icon: AppAssets.my_radiology_icon, + title: getIt.get().isArabic() ? LocaleKeys.labSubtitle : LocaleKeys.radiology, + subtitle: getIt.get().isArabic() ? LocaleKeys.radiology : LocaleKeys.labSubtitle, + backgroundColor: AppColors.whiteColor, + iconColor: AppColors.blackColor, + textColor: AppColors.blackColor, + isBold: false, + ), + ServiceCardData( + serviceName: "prescriptions", + icon: AppAssets.my_prescription_icon, + title: getIt.get().isArabic() ? LocaleKeys.medicinesSubtitle : LocaleKeys.medicalKey, + subtitle: getIt.get().isArabic() ? LocaleKeys.medicalKey : LocaleKeys.medicinesSubtitle, + backgroundColor: AppColors.whiteColor, + iconColor: AppColors.blackColor, + textColor: AppColors.blackColor, + isBold: false, + ), + ServiceCardData( + serviceName: "insurance_update", + icon: AppAssets.insurance_update_icon, + title: getIt.get().isArabic() ? LocaleKeys.insurance : LocaleKeys.insurance, + subtitle: getIt.get().isArabic() ? LocaleKeys.medical : LocaleKeys.details2, + backgroundColor: AppColors.whiteColor, + iconColor: AppColors.blackColor, + textColor: AppColors.blackColor, + isBold: false, + ), + ServiceCardData( + serviceName: "my_doctors", + icon: AppAssets.my_doctors_icon, + title: getIt.get().isArabic() ? LocaleKeys.myDoctorSubtitle2.tr() : "My", + subtitle: getIt.get().isArabic() ? LocaleKeys.myDoctor.tr() : "Doctors", + backgroundColor: AppColors.whiteColor, + iconColor: AppColors.blackColor, + textColor: AppColors.blackColor, + isBold: false, + ), + ServiceCardData( + serviceName: "sick_leaves", + icon: AppAssets.my_sick_leave_icon, + title: getIt.get().isArabic() ? LocaleKeys.sickSubtitle : LocaleKeys.sick2, + subtitle: getIt.get().isArabic() ? LocaleKeys.sick2 : LocaleKeys.sickSubtitle, + backgroundColor: AppColors.whiteColor, + iconColor: AppColors.blackColor, + textColor: AppColors.blackColor, + isBold: false, + ), + ServiceCardData( + serviceName: "my_invoice", + icon: AppAssets.invoiceListIcon, + title: getIt.get().isArabic() ? LocaleKeys.myDoctorSubtitle2 : LocaleKeys.invoiceList2, + subtitle: getIt.get().isArabic() ? LocaleKeys.invoiceList2 : LocaleKeys.myDoctorSubtitle2, + backgroundColor: AppColors.whiteColor, + iconColor: AppColors.blackColor, + textColor: AppColors.blackColor, + isBold: false, + ), + ]; static List get getServiceCardsList => [ - ServiceCardData( - serviceName: "livecare", - icon: AppAssets.small_livecare_icon, - title: LocaleKeys.liveCare, - subtitle: LocaleKeys.liveCareServiceDesc, - largeCardIcon: AppAssets.liveCareService, - backgroundColor: AppColors.successColor, - iconColor: AppColors.whiteColor, - isBold: false, - isPNG: false - ), - // ServiceCardData( - // icon: AppAssets.homeBottom, - // title: "Dermatology", - // subtitle: "Explore our app, View our services and offers", - // largeCardIcon: AppAssets.homeBottom, - // backgroundColor: AppColors.primaryRedColor, - // isBold: false, - // ), - ServiceCardData( - serviceName: "home_health_care", - icon: AppAssets.homeBottom, - title: LocaleKeys.homeHealthCare, - subtitle: LocaleKeys.homeHealthCareText, - largeCardIcon: AppAssets.homeHealthCareService, - backgroundColor: AppColors.primaryRedColor, - iconColor: AppColors.whiteColor, - isBold: false, - isPNG: false - ), - ServiceCardData( - serviceName: "pharmacy", - icon: AppAssets.defaultMEP, //359846 - title: LocaleKeys.hmgPharmacy, - subtitle: LocaleKeys.hmgPharmacyText, - largeCardIcon: AppAssets.pharmacyService, - backgroundColor: AppColors.pharmacyBGColor, - iconColor: null, - isBold: true, - isPNG: true - ), - ]; + ServiceCardData( + serviceName: "livecare", + icon: AppAssets.small_livecare_icon, + title: LocaleKeys.liveCare, + subtitle: LocaleKeys.liveCareServiceDesc, + largeCardIcon: AppAssets.liveCareService, + backgroundColor: AppColors.successColor, + iconColor: AppColors.whiteColor, + isBold: false, + isPNG: false), + // ServiceCardData( + // icon: AppAssets.homeBottom, + // title: "Dermatology", + // subtitle: "Explore our app, View our services and offers", + // largeCardIcon: AppAssets.homeBottom, + // backgroundColor: AppColors.primaryRedColor, + // isBold: false, + // ), + ServiceCardData( + serviceName: "home_health_care", + icon: AppAssets.homeBottom, + title: LocaleKeys.homeHealthCare, + subtitle: LocaleKeys.homeHealthCareText, + largeCardIcon: AppAssets.homeHealthCareService, + backgroundColor: AppColors.primaryRedColor, + iconColor: AppColors.whiteColor, + isBold: false, + isPNG: false), + ServiceCardData( + serviceName: "pharmacy", + icon: AppAssets.defaultMEP, + //359846 + title: LocaleKeys.hmgPharmacy, + subtitle: LocaleKeys.hmgPharmacyText, + largeCardIcon: AppAssets.pharmacyService, + backgroundColor: AppColors.pharmacyBGColor, + iconColor: null, + isBold: true, + isPNG: true), + ]; } diff --git a/lib/presentation/home/landing_page.dart b/lib/presentation/home/landing_page.dart index fdb8afcf..8644655d 100644 --- a/lib/presentation/home/landing_page.dart +++ b/lib/presentation/home/landing_page.dart @@ -5,6 +5,7 @@ import 'dart:ui' as ui; import 'package:clarity_flutter/clarity_flutter.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; import 'package:flutter_swiper_view/flutter_swiper_view.dart'; import 'package:get_it/get_it.dart'; @@ -31,34 +32,30 @@ import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/ import 'package:hmg_patient_app_new/features/my_appointments/models/resp_models/patient_queue_details_response_model.dart'; import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_view_model.dart'; import 'package:hmg_patient_app_new/features/notifications/notifications_view_model.dart'; +import 'package:hmg_patient_app_new/features/occasion_theme/occasion_theme_view_model.dart'; import 'package:hmg_patient_app_new/features/profile_settings/profile_settings_view_model.dart'; -import 'package:hmg_patient_app_new/features/paytabs/paytabs_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/appointments/appointment_queue_page.dart'; import 'package:hmg_patient_app_new/presentation/appointments/my_appointments_page.dart'; import 'package:hmg_patient_app_new/presentation/appointments/widgets/appointment_card.dart'; -import 'package:hmg_patient_app_new/presentation/authentication/login.dart'; import 'package:hmg_patient_app_new/presentation/authentication/quick_login.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/book_appointment_page.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/livecare/immediate_livecare_pending_request_page.dart'; import 'package:hmg_patient_app_new/presentation/contact_us/contact_us.dart'; import 'package:hmg_patient_app_new/presentation/hmg_services/services_page.dart'; -import 'package:hmg_patient_app_new/presentation/home/app_update_page.dart'; import 'package:hmg_patient_app_new/presentation/home/data/landing_page_data.dart'; import 'package:hmg_patient_app_new/presentation/home/widgets/habib_wallet_card.dart'; import 'package:hmg_patient_app_new/presentation/home/widgets/lakum_wallet_card.dart'; import 'package:hmg_patient_app_new/presentation/home/widgets/large_service_card.dart'; +import 'package:hmg_patient_app_new/presentation/home/widgets/occasion_banner_widget.dart'; import 'package:hmg_patient_app_new/presentation/home/widgets/small_service_card.dart'; import 'package:hmg_patient_app_new/presentation/home/widgets/welcome_widget.dart'; import 'package:hmg_patient_app_new/presentation/insurance/widgets/insurance_update_details_card.dart'; import 'package:hmg_patient_app_new/presentation/medical_file/medical_file_page.dart'; import 'package:hmg_patient_app_new/presentation/my_family/my_family.dart'; import 'package:hmg_patient_app_new/presentation/notifications/notifications_list_page.dart'; -import 'package:hmg_patient_app_new/presentation/offers_and_discounts/offers_and_discounts_page.dart'; -import 'package:hmg_patient_app_new/presentation/offers_and_discounts/widgets/offers_and_discounts.dart'; import 'package:hmg_patient_app_new/presentation/rate_appointment/rate_appointment_doctor.dart'; -import 'package:hmg_patient_app_new/presentation/tele_consultation/zoom/call_screen.dart'; import 'package:hmg_patient_app_new/presentation/todo_section/ancillary_procedures_details_page.dart'; import 'package:hmg_patient_app_new/presentation/todo_section/todo_page.dart'; import 'package:hmg_patient_app_new/presentation/todo_section/widgets/ancillary_orders_list.dart'; @@ -126,10 +123,7 @@ class _LandingPageState extends State { authVM.savePushTokenToAppState(); if (mounted) { final user = appState.getAuthenticatedUser(); - final hasUserQuickLoginData = - user != null && - (user.mobileNumber?.isNotEmpty ?? false) && - (user.patientIdentificationNo?.isNotEmpty ?? false); + final hasUserQuickLoginData = user != null && (user.mobileNumber?.isNotEmpty ?? false) && (user.patientIdentificationNo?.isNotEmpty ?? false); if (hasUserQuickLoginData) { authVM.checkLastLoginStatus(() { @@ -191,7 +185,7 @@ class _LandingPageState extends State { onSuccess: ((response) { // Only open dialog if details are loaded AND widget is mounted if (!mounted || appointmentRatingViewModel.appointmentDetails == null) return; - + appointmentRatingViewModel.setClinicOrDoctor(false); appointmentRatingViewModel.setTitle(LocaleKeys.rateDoctor.tr()); appointmentRatingViewModel.setSubTitle(LocaleKeys.howWasYourLastVisitWithDoctor.tr()); @@ -227,585 +221,695 @@ class _LandingPageState extends State { child: Scaffold( backgroundColor: AppColors.bgScaffoldColor, body: Consumer(builder: (context, insuranceVM, child) { + // Only when an occasion theme is active AND it publishes a header + // banner do we switch to the sticky-header layout below. Otherwise + // (no occasion, or occasion active but no banner configured) the + // page behaves exactly as it always has — banner (if any) scrolls + // inline with the rest of the content. + final hasStickyBanner = context.watch().headerImage?.isNotEmpty ?? false; + return Stack( children: [ - RefreshIndicator( - color: AppColors.primaryRedColor, - onRefresh: () async { - if (appState.isAuthenticated) { - // Refresh Appointments Data - myAppointmentsViewModel.setIsAppointmentDataToBeLoaded(true); - myAppointmentsViewModel.initAppointmentsViewModel(); - myAppointmentsViewModel.getPatientAppointments(true, false, isForTimeLine: true); - - // Refresh Appointments Data - // habibWalletVM.initHabibWalletProvider(); - // habibWalletVM.getPatientBalanceAmount(); - // habibWalletVM.getLakumAccountInformation(); - - // Refresh Ancillary Orders - todoSectionViewModel.setIsAncillaryOrdersNeedReloading(true); - todoSectionViewModel.initializeTodoSectionViewModel(); - - // Refresh Immediate LiveCare Data - immediateLiveCareViewModel.initImmediateLiveCare(); - immediateLiveCareViewModel.getPatientLiveCareHistory(); - - // 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); - // }), - // ); - // } - // }, - // ); - - } - }, - child: SingleChildScrollView( - controller: _scrollController, - physics: const AlwaysScrollableScrollPhysics(), - padding: EdgeInsets.only( - top: (appState.isAuthenticated && !insuranceVM.isInsuranceLoading && insuranceVM.isInsuranceExpired && insuranceVM.isInsuranceExpiryBannerShown) - ? (MediaQuery.paddingOf(context).top + 70.h) - : kToolbarHeight + 16.h, - bottom: 24.h), - child: Column( - spacing: 16.h, - children: [ - Row( - spacing: 8.h, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - appState.isAuthenticated - ? WelcomeWidget( - onTap: () { - // ...existing navigation code... - - Navigator.of(context).push( - CustomPageRoute( - direction: AxisDirection.down, - page: FamilyMedicalScreen(), - ), - ); - - // Navigator.pushReplacementNamed( - // // context, - // GetIt.instance().navigatorKey.currentContext!, - // AppRoutes.zoomCallPage, - // // arguments: CallArguments(appointmentID, "111", "Patient", "40", "1", true, 1), - // arguments: CallArguments("yosemite-338", "123", "Patient", "40", "0", true, 1), - // // arguments: CallArguments("SmallDailyStandup9875", "123", "Patient", "40", "0", false, int.parse(widget.incomingCallData!.appointmentNo!)), - // ); - }, - name: ('${appState.getAuthenticatedUser()!.firstName!} ${appState.getAuthenticatedUser()!.lastName!}'), - imageWidget: Selector( - selector: (_, profileVM) => profileVM.profileImageData ?? appState.getProfileImageData, - shouldRebuild: (previous, next) => previous != next, - builder: (context, profileImageData, child) { - // Only rebuild when profile image data changes - return UserAvatarWidget( - key: ValueKey('landing_avatar_${profileImageData?.hashCode ?? 0}'), - width: 42.w, - height: 42.h, - fit: BoxFit.cover, - isCircular: true, - customProfileImageData: profileImageData, - ); - }, - ), - ).expanded - : CustomButton( - text: LocaleKeys.loginOrRegister.tr(context: context), - onPressed: () async { - await authVM.onLoginPressed(); - - // Navigator.pushReplacementNamed( - // // context, - // GetIt.instance().navigatorKey.currentContext!, - // AppRoutes.zoomCallPage, - // arguments: CallArguments("lake-tahoe-289", "123", "Patient", "40", "0", true, 1), - // ); - - // Navigator.of(context).push( - // CustomPageRoute( - // // page: NotificationsListPage(), - // page: AppUpdatePage( - // appUpdateMessage: - // "A new update for Dr.Alhabib app is now available with new features and services.\r\nFor technical support:\r\neservices.hmg@drsulaimanalhabib.com"), - // ), - // ); - - // ...existing commented code... - }, - backgroundColor: AppColors.secondaryLightRedColor, - borderColor: AppColors.secondaryLightRedColor, - textColor: AppColors.primaryRedColor, - fontSize: 14.f, - fontWeight: FontWeight.w700, - borderRadius: 12.r, - padding: EdgeInsets.fromLTRB(12.h, 0, 12.h, 0), - height: 40.h, - ), - Consumer(builder: (context, todoSectionVM, child) { - return Row( - mainAxisSize: MainAxisSize.min, - // spacing: 18.h, + // Renders nothing unless a remote-config occasion theme (e.g. + // National Day) is currently active — fills the whole page, + // behind all other content below. + const OccasionBackgroundWidget(), + + if (hasStickyBanner) + // Android 15+ (targetSdk 36) enforces edge-to-edge and + // silently ignores SystemUiOverlayStyle.statusBarColor, so + // the status bar's background must be painted manually. + // AppColors.primaryRedColor already resolves to the active + // occasion's color, so this needs no extra plumbing. + Positioned( + top: 0, + left: 0, + right: 0, + child: Container( + height: MediaQuery.paddingOf(context).top, + color: AppColors.primaryRedColor, + ), + ), + + Column( + children: [ + // Pinned/sticky header — only rendered here when an + // occasion banner is active; scrollable content below + // fills the remaining space and scrolls independently. + // Only the banner itself is offset clear of the status + // bar — the scrollable content underneath intentionally + // keeps extending edge-to-edge behind it (unchanged from + // before), with the AnimatedOpacity gradient smoothing + // that transition as the user scrolls. + if (hasStickyBanner) + Padding( + padding: EdgeInsets.only(top: MediaQuery.paddingOf(context).top - 12.h), + child: const OccasionBannerWidget(), + ), + Expanded( + child: RefreshIndicator( + color: AppColors.primaryRedColor, + onRefresh: () async { + // Re-check Remote Config for occasion theme changes on + // pull-to-refresh (independent of auth state) — handy for + // testing without needing to restart the app. + unawaited(GetIt.instance().refresh()); + + if (appState.isAuthenticated) { + // Refresh Appointments Data + myAppointmentsViewModel.setIsAppointmentDataToBeLoaded(true); + myAppointmentsViewModel.initAppointmentsViewModel(); + myAppointmentsViewModel.getPatientAppointments(true, false, isForTimeLine: true); + + // Refresh Appointments Data + // habibWalletVM.initHabibWalletProvider(); + // habibWalletVM.getPatientBalanceAmount(); + // habibWalletVM.getLakumAccountInformation(); + + // Refresh Ancillary Orders + todoSectionViewModel.setIsAncillaryOrdersNeedReloading(true); + todoSectionViewModel.initializeTodoSectionViewModel(); + + // Refresh Immediate LiveCare Data + immediateLiveCareViewModel.initImmediateLiveCare(); + immediateLiveCareViewModel.getPatientLiveCareHistory(); + + // 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); + // }), + // ); + // } + // }, + // ); + } + }, + child: SingleChildScrollView( + controller: _scrollController, + physics: const AlwaysScrollableScrollPhysics(), + padding: EdgeInsets.only( + top: (appState.isAuthenticated && !insuranceVM.isInsuranceLoading && insuranceVM.isInsuranceExpired && insuranceVM.isInsuranceExpiryBannerShown) + ? (MediaQuery.paddingOf(context).top + 70.h) + // When the occasion banner is pinned above (hasStickyBanner), it + // already occupies the toolbar-height space that this padding + // otherwise reserves — so only a small breathing gap is needed. + : (hasStickyBanner ? 16.h : kToolbarHeight + 16.h), + bottom: 24.h), + child: Column( + children: [ + if (!hasStickyBanner) const OccasionBannerWidget(), + Row( + spacing: 8.h, + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Stack(clipBehavior: Clip.none, children: [ - if (appState.isAuthenticated) Utils.buildSvgWithAssets(icon: AppAssets.bell, height: 24.h, width: 24.h), - (appState.isAuthenticated && todoSectionVM.notificationsCount != null && todoSectionVM.notificationsCount != "0") - ? Positioned( - // right: appState.isArabic() ? 8.w : -8.w, - top: -8.h, - // left: 4.h, - // bottom: 4.h, - child: Container( - width: 18.w, - height: 18.h, - padding: EdgeInsets.all(2), - decoration: BoxDecoration( - color: AppColors.primaryRedColor, - borderRadius: BorderRadius.circular(20.r), + appState.isAuthenticated + ? WelcomeWidget( + onTap: () { + // ...existing navigation code... + + Navigator.of(context).push( + CustomPageRoute( + direction: AxisDirection.down, + page: FamilyMedicalScreen(), ), - child: Text( - todoSectionVM.notificationsCount ?? "0", - style: TextStyle( - color: Colors.white, - fontFamily: "Poppins", - fontSize: 10.f, - fontWeight: FontWeight.w600, - ), - textAlign: TextAlign.center, + ); + + // Navigator.pushReplacementNamed( + // // context, + // GetIt.instance().navigatorKey.currentContext!, + // AppRoutes.zoomCallPage, + // // arguments: CallArguments(appointmentID, "111", "Patient", "40", "1", true, 1), + // arguments: CallArguments("yosemite-338", "123", "Patient", "40", "0", true, 1), + // // arguments: CallArguments("SmallDailyStandup9875", "123", "Patient", "40", "0", false, int.parse(widget.incomingCallData!.appointmentNo!)), + // ); + }, + name: ('${appState.getAuthenticatedUser()!.firstName!} ${appState.getAuthenticatedUser()!.lastName!}'), + imageWidget: Selector( + selector: (_, profileVM) => profileVM.profileImageData ?? appState.getProfileImageData, + shouldRebuild: (previous, next) => previous != next, + builder: (context, profileImageData, child) { + // Only rebuild when profile image data changes + return UserAvatarWidget( + key: ValueKey('landing_avatar_${profileImageData?.hashCode ?? 0}'), + width: 42.w, + height: 42.h, + fit: BoxFit.cover, + isCircular: true, + customProfileImageData: profileImageData, + ); + }, + ), + ).expanded + : CustomButton( + text: LocaleKeys.loginOrRegister.tr(context: context), + onPressed: () async { + await authVM.onLoginPressed(); + + // Navigator.pushReplacementNamed( + // // context, + // GetIt.instance().navigatorKey.currentContext!, + // AppRoutes.zoomCallPage, + // arguments: CallArguments("lake-tahoe-289", "123", "Patient", "40", "0", true, 1), + // ); + + // Navigator.of(context).push( + // CustomPageRoute( + // // page: NotificationsListPage(), + // page: AppUpdatePage( + // appUpdateMessage: + // "A new update for Dr.Alhabib app is now available with new features and services.\r\nFor technical support:\r\neservices.hmg@drsulaimanalhabib.com"), + // ), + // ); + + // ...existing commented code... + }, + backgroundColor: AppColors.secondaryLightRedColor, + borderColor: Colors.transparent, + textColor: AppColors.primaryRedColor, + fontSize: 14.f, + fontWeight: FontWeight.w700, + borderRadius: 10.r, + padding: EdgeInsets.fromLTRB(12.h, 0, 12.h, 0), + height: 40.h, + ), + Consumer(builder: (context, todoSectionVM, child) { + return Row( + mainAxisSize: MainAxisSize.min, + // spacing: 18.h, + children: [ + Stack(clipBehavior: Clip.none, children: [ + if (appState.isAuthenticated) Utils.buildSvgWithAssets(icon: AppAssets.bell, height: 24.h, width: 24.h), + (appState.isAuthenticated && todoSectionVM.notificationsCount != null && todoSectionVM.notificationsCount != "0") + ? Positioned( + // right: appState.isArabic() ? 8.w : -8.w, + top: -8.h, + // left: 4.h, + // bottom: 4.h, + child: Container( + width: 18.w, + height: 18.h, + padding: EdgeInsets.all(2), + decoration: BoxDecoration( + color: AppColors.primaryRedColorBase, + borderRadius: BorderRadius.circular(20.r), + ), + child: Text( + todoSectionVM.notificationsCount ?? "0", + style: TextStyle( + color: Colors.white, + fontFamily: "Poppins", + fontSize: 10.f, + fontWeight: FontWeight.w600, + ), + textAlign: TextAlign.center, + ), + ), + ) + : SizedBox.shrink(), + ]).onPress(() async { + if (appState.isAuthenticated) { + notificationsViewModel.setNotificationStatusID(2); + notificationsViewModel.getAllNotifications(); + Navigator.of(context).push( + CustomPageRoute( + page: NotificationsListPage(), + // page: LoginScreen(), ), - ), - ) - : SizedBox.shrink(), - ]).onPress(() async { - if (appState.isAuthenticated) { - notificationsViewModel.setNotificationStatusID(2); - notificationsViewModel.getAllNotifications(); - Navigator.of(context).push( - CustomPageRoute( - page: NotificationsListPage(), - // page: LoginScreen(), + ); + } else { + await authVM.onLoginPressed(); + } + }), + SizedBox( + width: 24.w, ), - ); - } else { - await authVM.onLoginPressed(); - } - }), - SizedBox( - width: 24.w, - ), - Utils.buildSvgWithAssets(icon: AppAssets.location, height: 24.h, width: 24.w).onPress(() { - // openIndoorNavigationBottomSheet(context); - showCommonBottomSheetWithoutHeight( - context, - title: LocaleKeys.contactUs.tr(), - child: ContactUs(), - callBackFunc: () {}, - isFullScreen: false, + Utils.buildSvgWithAssets(icon: AppAssets.location, height: 24.h, width: 24.w).onPress(() { + // openIndoorNavigationBottomSheet(context); + showCommonBottomSheetWithoutHeight( + context, + title: LocaleKeys.contactUs.tr(), + child: ContactUs(), + callBackFunc: () {}, + isFullScreen: false, + ); + }), + + // Utils.buildSvgWithAssets(icon: AppAssets.contact_icon, height: 24.h, width: 24.h).onPress(() { + // showCommonBottomSheetWithoutHeight( + // context, + // title: LocaleKeys.contactUs.tr(), + // child: ContactUs(), + // callBackFunc: () {}, + // isFullScreen: false, + // ); + // }), + !appState.isAuthenticated + ? Row(children: [ + SizedBox(width: 24.w), + Utils.buildSvgWithAssets(icon: appState.isArabic() ? AppAssets.enLangIcon : AppAssets.arLangIcon, height: 24.h, width: 24.h).onPress(() { + context.setLocale(appState.isArabic() ? Locale('en', 'US') : Locale('ar', 'SA')); + }) + ]) + : SizedBox() + ], ); }), - - // Utils.buildSvgWithAssets(icon: AppAssets.contact_icon, height: 24.h, width: 24.h).onPress(() { - // showCommonBottomSheetWithoutHeight( - // context, - // title: LocaleKeys.contactUs.tr(), - // child: ContactUs(), - // callBackFunc: () {}, - // isFullScreen: false, - // ); - // }), - !appState.isAuthenticated - ? Row(children: [ - SizedBox(width: 24.w), - Utils.buildSvgWithAssets(icon: appState.isArabic() ? AppAssets.enLangIcon : AppAssets.arLangIcon, height: 24.h, width: 24.h).onPress(() { - context.setLocale(appState.isArabic() ? Locale('en', 'US') : Locale('ar', 'SA')); - }) - ]) - : SizedBox() ], - ); - }), - ], - ).paddingSymmetrical(24.h, 0.h), - !appState.isAuthenticated - ? Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 24.r, - hasShadow: false, - ), - child: Padding( - padding: EdgeInsets.all(16.h), - child: Row( - children: [ - Utils.buildSvgWithAssets( - width: 50.w, - height: 60.h, - icon: AppAssets.symptomCheckerIcon, - fit: BoxFit.contain, + ).paddingSymmetrical(24.h, 0.h), + SizedBox(height: 8.h), + !appState.isAuthenticated + ? Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: false, ), - SizedBox(width: 12.w), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - LocaleKeys.howAreYouFeelingToday.tr(context: context).toText14(isBold: true), - LocaleKeys.checkYourSymptomsWithScale.tr(context: context).toText12(isBold: true), - SizedBox(height: 14.h), - CustomButton( - text: LocaleKeys.checkYourSymptoms.tr(context: context), - onPressed: () async { - context.navigateWithName(AppRoutes.userInfoSelection); - }, - padding: EdgeInsetsGeometry.zero, - backgroundColor: AppColors.primaryRedColor, - borderColor: AppColors.primaryRedColor, - textColor: Colors.white, - fontSize: 14.f, - fontWeight: FontWeight.w600, - borderRadius: 12.r, - height: 40.h, - ), - ], - ).expanded - ], - ), - ), - ).paddingSymmetrical(24.w, 0.h) - : SizedBox.shrink(), - - // Offers And Discounts Carousel - Auto-scrolling from right to left - // appState.isAuthenticated && appState.isEnabledOffersAndDiscountsCarousel - // ? Column( - // children: [ - // SizedBox(height: 12.h), - // Row( - // mainAxisAlignment: MainAxisAlignment.spaceBetween, - // children: [ - // ("${LocaleKeys.offers.tr(context: context)} & ${LocaleKeys.discount.tr(context: context)}").toText16(isBold: true), - // Row( - // children: [ - // LocaleKeys.viewAll.tr(context: context).toText14(color: AppColors.primaryRedColor, isBold: true), - // 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: OffersAndDiscountsPage())); - // }), - // SizedBox(height: 16.h), - // OffersAndDiscountsCarousel().paddingSymmetrical(24.h, 0.h), - // SizedBox(height: 18.h), - // ], - // ) - // : SizedBox.shrink(), - - appState.isAuthenticated - ? Column( - children: [ - SizedBox(height: 12.h), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - LocaleKeys.appointmentsAndVisits.tr(context: context).toText16(isBold: true), - Row( - children: [ - LocaleKeys.viewAll.tr(context: context).toText14(color: AppColors.primaryRedColor, isBold: true), - SizedBox(width: 2.h), - Icon(Icons.arrow_forward_ios, color: AppColors.primaryRedColor, size: 14.h), - ], + child: Padding( + padding: EdgeInsets.all(16.h), + child: Row( + children: [ + Utils.buildSvgWithAssets( + width: 50.w, + height: 60.h, + icon: AppAssets.symptomCheckerIcon, + fit: BoxFit.contain, + ), + SizedBox(width: 12.w), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LocaleKeys.howAreYouFeelingToday.tr(context: context).toText14(isBold: true), + LocaleKeys.checkYourSymptomsWithScale.tr(context: context).toText12(isBold: true), + SizedBox(height: 14.h), + CustomButton( + text: LocaleKeys.checkYourSymptoms.tr(context: context), + onPressed: () async { + context.navigateWithName(AppRoutes.userInfoSelection); + }, + padding: EdgeInsetsGeometry.zero, + backgroundColor: AppColors.primaryRedColor, + borderColor: AppColors.primaryRedColor, + textColor: Colors.white, + fontSize: 14.f, + fontWeight: FontWeight.w600, + borderRadius: 12.r, + height: 40.h, + ), + ], + ).expanded + ], + ), ), - ], - ).paddingSymmetrical(24.h, 0.h).onPress(() { - myAppointmentsViewModel.onTabChange(0); - myAppointmentsViewModel.updateListWRTTab(0); - - Navigator.of(context).push(CustomPageRoute(page: MyAppointmentsPage())); - // getIt.get().setPaymentConfiguration("Appointment Payment", 15.5); - // getIt.get().startCardPayment(); - }), - Consumer3( - builder: (context, myAppointmentsVM, immediateLiveCareVM, todoSectionVM, child) { - return myAppointmentsVM.isMyAppointmentsLoading - ? Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 24.r, - hasShadow: true, - ), - child: AppointmentCard( - patientAppointmentHistoryResponseModel: PatientAppointmentHistoryResponseModel(), - myAppointmentsViewModel: myAppointmentsViewModel, - bookAppointmentsViewModel: bookAppointmentsViewModel, - isLoading: true, - isFromHomePage: true, - ), - ).paddingSymmetrical(24.h, 16.h) - : myAppointmentsVM.patientAppointmentsHistoryList.isNotEmpty - ? myAppointmentsVM.patientAppointmentsHistoryList.length == 1 && !immediateLiveCareVM.patientHasPendingLiveCareRequest - ? Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 24.r, - hasShadow: true, - ), - child: AppointmentCard( - patientAppointmentHistoryResponseModel: myAppointmentsVM.patientAppointmentsHistoryList.first, - myAppointmentsViewModel: myAppointmentsViewModel, - bookAppointmentsViewModel: bookAppointmentsViewModel, - isLoading: false, - isFromHomePage: true, - ), - ).paddingSymmetrical(24.h, 0.h) - : isTablet - ? SizedBox( - height: isTablet ? 290.h : 255.h, - child: ListView.separated( - scrollDirection: Axis.horizontal, - itemCount: () { - final int baseCount = - myAppointmentsVM.patientAppointmentsHistoryList.length + (immediateLiveCareVM.patientHasPendingLiveCareRequest ? 1 : 0); - return baseCount < 3 ? baseCount : 3; - }(), - shrinkWrap: true, - padding: EdgeInsets.only(left: 16.h, right: 16.h), - itemBuilder: (context, index) { - return SizedBox( - height: isTablet ? 290.h : 255.h, - width: 250.w, - child: getIndexSwiperCard(index), - ); - // return AnimationConfiguration.staggeredList( - // position: index, - // duration: const Duration(milliseconds: 1000), - // child: SlideAnimation( - // horizontalOffset: 100.0, - // child: FadeInAnimation( - // child: SizedBox( - // height: 255.h, - // width: 250.w, - // child: getIndexSwiperCard(index), - // ), - // ), - // ), - // ); - }, - separatorBuilder: (BuildContext cxt, int index) => SizedBox( - width: 10.w, + ).paddingSymmetrical(24.w, 0.h) + : SizedBox.shrink(), + SizedBox(height: 16.h), + + // Offers And Discounts Carousel - Auto-scrolling from right to left + // appState.isAuthenticated && appState.isEnabledOffersAndDiscountsCarousel + // ? Column( + // children: [ + // SizedBox(height: 12.h), + // Row( + // mainAxisAlignment: MainAxisAlignment.spaceBetween, + // children: [ + // ("${LocaleKeys.offers.tr(context: context)} & ${LocaleKeys.discount.tr(context: context)}").toText16(isBold: true), + // Row( + // children: [ + // LocaleKeys.viewAll.tr(context: context).toText14(color: AppColors.primaryRedColor, isBold: true), + // 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: OffersAndDiscountsPage())); + // }), + // SizedBox(height: 16.h), + // OffersAndDiscountsCarousel().paddingSymmetrical(24.h, 0.h), + // SizedBox(height: 18.h), + // ], + // ) + // : SizedBox.shrink(), + + appState.isAuthenticated + ? Column( + children: [ + SizedBox(height: 12.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + LocaleKeys.appointmentsAndVisits.tr(context: context).toText16(isBold: true), + Row( + children: [ + LocaleKeys.viewAll.tr(context: context).toText14(color: AppColors.primaryRedColor, isBold: true), + SizedBox(width: 2.h), + Icon(Icons.arrow_forward_ios, color: AppColors.primaryRedColor, size: 14.h), + ], + ), + ], + ).paddingSymmetrical(24.h, 0.h).onPress(() { + myAppointmentsViewModel.onTabChange(0); + myAppointmentsViewModel.updateListWRTTab(0); + + Navigator.of(context).push(CustomPageRoute(page: MyAppointmentsPage())); + // getIt.get().setPaymentConfiguration("Appointment Payment", 15.5); + // getIt.get().startCardPayment(); + }), + Consumer3( + builder: (context, myAppointmentsVM, immediateLiveCareVM, todoSectionVM, child) { + return myAppointmentsVM.isMyAppointmentsLoading + ? Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: true, + ), + child: AppointmentCard( + patientAppointmentHistoryResponseModel: PatientAppointmentHistoryResponseModel(), + myAppointmentsViewModel: myAppointmentsViewModel, + bookAppointmentsViewModel: bookAppointmentsViewModel, + isLoading: true, + isFromHomePage: true, + ), + ).paddingSymmetrical(24.h, 16.h) + : myAppointmentsVM.patientAppointmentsHistoryList.isNotEmpty + ? myAppointmentsVM.patientAppointmentsHistoryList.length == 1 && !immediateLiveCareVM.patientHasPendingLiveCareRequest + ? Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration( + color: AppColors.whiteColor, + borderRadius: 24.r, + hasShadow: true, ), - ), - ) - : SizedBox( - height: 255.h + 20 + 30, // itemHeight + shadow padding (10 top + 10 bottom) + pagination dots space - child: Builder( - builder: (context) { - final int baseAppointmentCount = myAppointmentsVM.patientAppointmentsHistoryList.length; - final int liveCareCount = immediateLiveCareVM.patientHasPendingLiveCareRequest ? 1 : 0; - final int totalCount = baseAppointmentCount + liveCareCount; - - final int swiperItemCount = myAppointmentsVM.isMyAppointmentsLoading - ? 3 - : totalCount < 3 - ? totalCount - : 3; - final bool isArabic = appState.isArabic(); - return Swiper( - itemCount: swiperItemCount, - layout: SwiperLayout.STACK, - loop: false, - itemWidth: MediaQuery.of(context).size.width - 48.h, - indicatorLayout: PageIndicatorLayout.COLOR, - axisDirection: isArabic ? AxisDirection.right : AxisDirection.left, - controller: _controller, - index: isArabic ? swiperItemCount - 1 : 0, - itemHeight: 255.h + 20, - // extra space for shadow - pagination: SwiperPagination( - alignment: Alignment.bottomCenter, - margin: EdgeInsets.only(top: 220.h + 20 + 8 + 24), - builder: _DirectionalDotPaginationBuilder( - color: Color(0xffD9D9D9), - activeColor: AppColors.blackBgColor, - isRTL: isArabic, - totalCount: swiperItemCount, + child: AppointmentCard( + patientAppointmentHistoryResponseModel: myAppointmentsVM.patientAppointmentsHistoryList.first, + myAppointmentsViewModel: myAppointmentsViewModel, + bookAppointmentsViewModel: bookAppointmentsViewModel, + isLoading: false, + isFromHomePage: true, + ), + ).paddingSymmetrical(24.h, 0.h) + : isTablet + ? SizedBox( + height: isTablet ? 290.h : 255.h, + child: ListView.separated( + scrollDirection: Axis.horizontal, + itemCount: () { + final int baseCount = + myAppointmentsVM.patientAppointmentsHistoryList.length + (immediateLiveCareVM.patientHasPendingLiveCareRequest ? 1 : 0); + return baseCount < 3 ? baseCount : 3; + }(), + shrinkWrap: true, + padding: EdgeInsets.only(left: 16.h, right: 16.h), + itemBuilder: (context, index) { + return SizedBox( + height: isTablet ? 290.h : 255.h, + width: 250.w, + child: getIndexSwiperCard(index), + ); + // return AnimationConfiguration.staggeredList( + // position: index, + // duration: const Duration(milliseconds: 1000), + // child: SlideAnimation( + // horizontalOffset: 100.0, + // child: FadeInAnimation( + // child: SizedBox( + // height: 255.h, + // width: 250.w, + // child: getIndexSwiperCard(index), + // ), + // ), + // ), + // ); + }, + separatorBuilder: (BuildContext cxt, int index) => SizedBox( + width: 10.w, ), ), - itemBuilder: (BuildContext context, int index) { - final int actualIndex = isArabic ? (swiperItemCount - 1 - index) : index; - return Padding( - padding: const EdgeInsets.symmetric(vertical: 10), - child: getIndexSwiperCard(actualIndex), - ); - }, - ); - }, + ) + : SizedBox( + height: 255.h + 20 + 30, // itemHeight + shadow padding (10 top + 10 bottom) + pagination dots space + child: Builder( + builder: (context) { + final int baseAppointmentCount = myAppointmentsVM.patientAppointmentsHistoryList.length; + final int liveCareCount = immediateLiveCareVM.patientHasPendingLiveCareRequest ? 1 : 0; + final int totalCount = baseAppointmentCount + liveCareCount; + + final int swiperItemCount = myAppointmentsVM.isMyAppointmentsLoading + ? 3 + : totalCount < 3 + ? totalCount + : 3; + final bool isArabic = appState.isArabic(); + return Swiper( + itemCount: swiperItemCount, + layout: SwiperLayout.STACK, + loop: false, + itemWidth: MediaQuery.of(context).size.width - 48.h, + indicatorLayout: PageIndicatorLayout.COLOR, + axisDirection: isArabic ? AxisDirection.right : AxisDirection.left, + controller: _controller, + index: isArabic ? swiperItemCount - 1 : 0, + itemHeight: 255.h + 20, + // extra space for shadow + pagination: SwiperPagination( + alignment: Alignment.bottomCenter, + margin: EdgeInsets.only(top: 220.h + 20 + 8 + 24), + builder: _DirectionalDotPaginationBuilder( + color: Color(0xffD9D9D9), + activeColor: AppColors.blackBgColor, + isRTL: isArabic, + totalCount: swiperItemCount, + ), + ), + itemBuilder: (BuildContext context, int index) { + final int actualIndex = isArabic ? (swiperItemCount - 1 - index) : index; + return Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: getIndexSwiperCard(actualIndex), + ); + }, + ); + }, + ), + ) + : immediateLiveCareVM.patientHasPendingLiveCareRequest + ? _buildLiveCareRequestCard().paddingSymmetrical(24.h, 16.h) + : Container( + width: double.infinity, + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r, hasShadow: true), + child: Padding( + padding: EdgeInsets.all(16.h), + child: Column( + children: [ + Utils.buildSvgWithAssets(icon: AppAssets.home_calendar_icon, width: 32.h, height: 32.h), + SizedBox(height: 12.h), + LocaleKeys.noUpcomingAppointmentPleaseBook.tr(context: context).toText12(isCenter: true), + SizedBox(height: 12.h), + CustomButton( + text: LocaleKeys.bookAppo.tr(context: context), + onPressed: () { + getIt.get().onTabChanged(0); + Navigator.of(context).push(CustomPageRoute(page: BookAppointmentPage())); + }, + backgroundColor: AppColors.secondaryLightRedColor, + borderColor: AppColors.secondaryLightRedColor, + textColor: AppColors.primaryRedColor, + fontSize: 14.f, + fontWeight: FontWeight.w600, + padding: EdgeInsets.fromLTRB(10.h, 0, 10.h, 0), + icon: AppAssets.add_icon, + iconColor: AppColors.primaryRedColor, + height: 40.h, + ), + ], + ), + ), + ).paddingSymmetrical(24.h, 16.h); + }, + ), + + // 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), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + LocaleKeys.quickLinks.tr(context: context).toText16(isBold: true), + Row( + children: [ + LocaleKeys.viewMedicalFileLandingPage.tr(context: context).toText14(color: AppColors.primaryRedColor, isBold: true), + 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: MedicalFilePage())); + }), + SizedBox(height: 16.h), + Consumer(builder: (BuildContext context, TodoSectionViewModel todoSectionVM, Widget? child) { + return Container( + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r), + child: Column( + children: [ + todoSectionVM.patientAncillaryOrdersList.isNotEmpty + ? Container( + height: 50.h, + decoration: ShapeDecoration( + color: AppColors.eReferralCardColor.withAlpha(50), + shape: SmoothRectangleBorder( + borderRadius: BorderRadius.only(topLeft: Radius.circular(24), topRight: Radius.circular(24)), + smoothness: 1, ), - ) - : immediateLiveCareVM.patientHasPendingLiveCareRequest - ? _buildLiveCareRequestCard().paddingSymmetrical(24.h, 16.h) - : Container( - width: double.infinity, - decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r, hasShadow: true), - child: Padding( - padding: EdgeInsets.all(16.h), - child: Column( + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Utils.buildSvgWithAssets(icon: AppAssets.home_calendar_icon, width: 32.h, height: 32.h), - SizedBox(height: 12.h), - LocaleKeys.noUpcomingAppointmentPleaseBook.tr(context: context).toText12(isCenter: true), - SizedBox(height: 12.h), + LocaleKeys.pendingAncillaryOrders + .tr(context: context) + .toText14(color: AppColors.eReferralCardColor, isBold: true) + .paddingSymmetrical(24.h, 0.h), CustomButton( - text: LocaleKeys.bookAppo.tr(context: context), + text: LocaleKeys.view.tr(context: context), onPressed: () { - getIt.get().onTabChanged(0); - Navigator.of(context).push(CustomPageRoute(page: BookAppointmentPage())); + todoSectionVM.setIsAncillaryOrdersNeedReloading(true); + Navigator.of(context).push(CustomPageRoute(page: ToDoPage())); }, - backgroundColor: Color(0xffFEE9EA), - borderColor: Color(0xffFEE9EA), - textColor: Color(0xffED1C2B), - fontSize: 14.f, + backgroundColor: AppColors.eReferralCardColor, + borderColor: AppColors.eReferralCardColor, + textColor: AppColors.whiteColor, + fontSize: 10.f, fontWeight: FontWeight.w600, - padding: EdgeInsets.fromLTRB(10.h, 0, 10.h, 0), - icon: AppAssets.add_icon, - iconColor: AppColors.primaryRedColor, - height: 40.h, - ), + borderRadius: 8, + padding: EdgeInsets.fromLTRB(15, 0, 15, 0), + height: 30.h, + ).paddingSymmetrical(24.h, 0.h), ], ), - ), - ).paddingSymmetrical(24.h, 16.h); - }, - ), - - // 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), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - LocaleKeys.quickLinks.tr(context: context).toText16(isBold: true), - Row( - children: [ - LocaleKeys.viewMedicalFileLandingPage.tr(context: context).toText14(color: AppColors.primaryRedColor, isBold: true), - 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: MedicalFilePage())); - }), - SizedBox(height: 16.h), - Consumer(builder: (BuildContext context, TodoSectionViewModel todoSectionVM, Widget? child) { - return Container( + ) + : SizedBox.shrink(), + 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: 200), + 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), + ), + ), + SizedBox(height: 16.h), + ], + ), + ).paddingSymmetrical(24.h, 0.h); + }), + ], + ) + : Container( decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r), child: Column( children: [ - todoSectionVM.patientAncillaryOrdersList.isNotEmpty - ? Container( - height: 50.h, - decoration: ShapeDecoration( - color: AppColors.eReferralCardColor.withAlpha(50), - shape: SmoothRectangleBorder( - borderRadius: BorderRadius.only(topLeft: Radius.circular(24), topRight: Radius.circular(24)), - smoothness: 1, - ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - LocaleKeys.pendingAncillaryOrders.tr(context: context).toText14(color: AppColors.eReferralCardColor, isBold: true).paddingSymmetrical(24.h, 0.h), - CustomButton( - text: LocaleKeys.view.tr(context: context), - onPressed: () { - todoSectionVM.setIsAncillaryOrdersNeedReloading(true); - Navigator.of(context).push(CustomPageRoute(page: ToDoPage())); - }, - backgroundColor: AppColors.eReferralCardColor, - borderColor: AppColors.eReferralCardColor, - textColor: AppColors.whiteColor, - fontSize: 10.f, - fontWeight: FontWeight.w600, - borderRadius: 8, - padding: EdgeInsets.fromLTRB(15, 0, 15, 0), - height: 30.h, - ).paddingSymmetrical(24.h, 0.h), - ], - ), - ) - : SizedBox.shrink(), SizedBox( height: 92.h + 32.h - 4.h, child: RawScrollbar( @@ -820,160 +924,112 @@ class _LandingPageState extends State { 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, + itemCount: LandingPageData.getNotLoggedInServiceCardsList.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: 200), + 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, + serviceName: LandingPageData.getNotLoggedInServiceCardsList[index].serviceName, + icon: LandingPageData.getNotLoggedInServiceCardsList[index].icon, + title: LandingPageData.getNotLoggedInServiceCardsList[index].title, + subtitle: LandingPageData.getNotLoggedInServiceCardsList[index].subtitle, + iconColor: LandingPageData.getNotLoggedInServiceCardsList[index].iconColor!, + textColor: LandingPageData.getNotLoggedInServiceCardsList[index].textColor, + backgroundColor: LandingPageData.getNotLoggedInServiceCardsList[index].backgroundColor, + isBold: LandingPageData.getNotLoggedInServiceCardsList[index].isBold, ), ), ), ); }, - separatorBuilder: (BuildContext cxt, int index) => 10.width, + separatorBuilder: (BuildContext cxt, int index) => 0.width, ).paddingSymmetrical(16.h, 0.h), ), ), SizedBox(height: 16.h), ], ), - ).paddingSymmetrical(24.h, 0.h); + ).paddingSymmetrical(24.h, 0.h), + SizedBox(height: 16.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + LocaleKeys.services2.tr(context: context).toText18(isBold: true), + Row( + children: [ + LocaleKeys.viewAll.tr(context: context).toText14(color: AppColors.primaryRedColor, isBold: true), + SizedBox(width: 2.h), + Icon(Icons.arrow_forward_ios, color: AppColors.primaryRedColor, size: 14.h), + ], + ).onPress(() { + Navigator.of(context).push(CustomPageRoute(page: ServicesPage())); }), ], - ) - : Container( - 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.getNotLoggedInServiceCardsList.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( - serviceName: LandingPageData.getNotLoggedInServiceCardsList[index].serviceName, - icon: LandingPageData.getNotLoggedInServiceCardsList[index].icon, - title: LandingPageData.getNotLoggedInServiceCardsList[index].title, - subtitle: LandingPageData.getNotLoggedInServiceCardsList[index].subtitle, - iconColor: LandingPageData.getNotLoggedInServiceCardsList[index].iconColor!, - textColor: LandingPageData.getNotLoggedInServiceCardsList[index].textColor, - backgroundColor: LandingPageData.getNotLoggedInServiceCardsList[index].backgroundColor, - isBold: LandingPageData.getNotLoggedInServiceCardsList[index].isBold, - ), - ), - ), - ); - }, - separatorBuilder: (BuildContext cxt, int index) => 0.width, - ).paddingSymmetrical(16.h, 0.h), + ).paddingSymmetrical(24.w, 0.h), + SizedBox(height: 16.h), + ConstrainedBox( + constraints: BoxConstraints(maxHeight: isFoldable ? 450.h : (isTablet ? 440.h : 431.h), minHeight: 411.h), + child: ListView.separated( + scrollDirection: Axis.horizontal, + itemCount: LandingPageData.getServiceCardsList.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: FadedLargeServiceCard( + serviceCardData: LandingPageData.getServiceCardsList[index], + image: LandingPageData.getServiceCardsList[index].icon, + title: LandingPageData.getServiceCardsList[index].title, + subtitle: LandingPageData.getServiceCardsList[index].subtitle, + icon: LandingPageData.getServiceCardsList[index].largeCardIcon, + isPNG: LandingPageData.getServiceCardsList[index].isPNG, + ), + ), ), - ), - SizedBox(height: 16.h), - ], - ), - ).paddingSymmetrical(24.h, 0.h), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - LocaleKeys.services2.tr(context: context).toText18(isBold: true), - Row( - children: [ - LocaleKeys.viewAll.tr(context: context).toText14(color: AppColors.primaryRedColor, isBold: true), - SizedBox(width: 2.h), - Icon(Icons.arrow_forward_ios, color: AppColors.primaryRedColor, size: 14.h), - ], - ).onPress(() { - Navigator.of(context).push(CustomPageRoute(page: ServicesPage())); - }), - ], - ).paddingSymmetrical(24.w, 0.h), - ConstrainedBox( - constraints: BoxConstraints(maxHeight: isFoldable ? 450.h : (isTablet ? 440.h : 431.h), minHeight: 411.h), - child: ListView.separated( - scrollDirection: Axis.horizontal, - itemCount: LandingPageData.getServiceCardsList.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: FadedLargeServiceCard( - serviceCardData: LandingPageData.getServiceCardsList[index], - image: LandingPageData.getServiceCardsList[index].icon, - title: LandingPageData.getServiceCardsList[index].title, - subtitle: LandingPageData.getServiceCardsList[index].subtitle, - icon: LandingPageData.getServiceCardsList[index].largeCardIcon, - isPNG: LandingPageData.getServiceCardsList[index].isPNG, - ), - ), + ); + }, + separatorBuilder: (BuildContext cxt, int index) => SizedBox(width: 16.w), ), - ); - }, - separatorBuilder: (BuildContext cxt, int index) => SizedBox(width: 16.w), + ), + SizedBox(height: 16.h), + appState.isAuthenticated + ? Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LocaleKeys.myBalanceSubtitle.tr(context: context).toText16(isBold: true).paddingSymmetrical(24.h, 0.h), + SizedBox( + height: 180.h, + child: ListView.separated( + scrollDirection: Axis.horizontal, + itemCount: 2, + padding: EdgeInsetsDirectional.only(start: 24.h, end: 24.h), + itemBuilder: (BuildContext context, int index) { + return index == 0 ? HabibWalletCard() : LakumWalletCard(); + }, + separatorBuilder: (BuildContext context, int index) => SizedBox(width: 12.h), + ), + ), + ], + ) + : SizedBox(), + ], ), ), - appState.isAuthenticated - ? Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - LocaleKeys.myBalanceSubtitle.tr(context: context).toText16(isBold: true).paddingSymmetrical(24.h, 0.h), - SizedBox( - height: 180.h, - child: ListView.separated( - scrollDirection: Axis.horizontal, - itemCount: 2, - padding: EdgeInsetsDirectional.only(start: 24.h, end: 24.h), - itemBuilder: (BuildContext context, int index) { - return index == 0 ? HabibWalletCard() : LakumWalletCard(); - }, - separatorBuilder: (BuildContext context, int index) => SizedBox(width: 12.h), - ), - ), - ], - ) - : SizedBox(), - ], + ), ), - ), + ], ), AnimatedOpacity( opacity: _scrollOpacity, @@ -998,9 +1054,9 @@ class _LandingPageState extends State { ? Container( height: MediaQuery.paddingOf(context).top + 50.h, decoration: ShapeDecoration( - color: AppColors.secondaryLightRedBorderColor, + color: AppColors.secondaryLightRedColorBase, shape: SmoothRectangleBorder( - side: BorderSide(width: 1, color: AppColors.primaryRedColor.withAlpha(20)), + side: BorderSide(width: 1, color: AppColors.primaryRedColorBase.withAlpha(20)), // borderRadius: BorderRadius.only(bottomLeft: Radius.circular(24), bottomRight: Radius.circular(24)), smoothness: 1, ), @@ -1011,7 +1067,7 @@ class _LandingPageState extends State { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - LocaleKeys.insuranceExpiredOrInactive.tr(context: context).toText14(color: AppColors.primaryRedColor, isBold: true).paddingSymmetrical(0.h, 0.h), + LocaleKeys.insuranceExpiredOrInactive.tr(context: context).toText14(color: AppColors.primaryRedColorBase, isBold: true).paddingSymmetrical(0.h, 0.h), Row( children: [ CustomButton( @@ -1023,8 +1079,8 @@ class _LandingPageState extends State { showCommonBottomSheetWithoutHeight(context, child: PatientInsuranceCardUpdateCard(), callBackFunc: () {}, title: "", isCloseButtonVisible: false, isFullScreen: false); }, - backgroundColor: AppColors.primaryRedColor, - borderColor: AppColors.secondaryLightRedBorderColor, + backgroundColor: AppColors.primaryRedColorBase, + borderColor: AppColors.primaryRedColorBase, textColor: Colors.white, fontSize: 12.f, fontWeight: FontWeight.bold, @@ -1032,7 +1088,7 @@ class _LandingPageState extends State { padding: EdgeInsets.fromLTRB(15, 0, 15, 0), height: 36.h, ).paddingSymmetrical(12.h, 0.h), - Icon(Icons.close, color: AppColors.primaryRedColor).onPress(() { + Icon(Icons.close, color: AppColors.primaryRedColorBase).onPress(() { insuranceVM.setIsInsuranceExpiryBannerShown(false); }), ], diff --git a/lib/presentation/home/widgets/habib_wallet_card.dart b/lib/presentation/home/widgets/habib_wallet_card.dart index 803b9b5a..604ed5f5 100644 --- a/lib/presentation/home/widgets/habib_wallet_card.dart +++ b/lib/presentation/home/widgets/habib_wallet_card.dart @@ -92,7 +92,7 @@ class HabibWalletCard extends StatelessWidget { ), SizedBox(width: 8.h), (habibWalletVM.isWalletAmountHidden - ? "••••" + ? "****" : NumberFormat.decimalPattern().format(habibWalletVM.habibWalletAmount).toString()) .toText32(isBold: true, isEnglishOnly: true) .toShimmer2(isShow: habibWalletVM.isWalletAmountLoading, radius: 12.h, width: 80.h, height: 40.h), diff --git a/lib/presentation/home/widgets/lakum_wallet_card.dart b/lib/presentation/home/widgets/lakum_wallet_card.dart index feea02ba..cddfd309 100644 --- a/lib/presentation/home/widgets/lakum_wallet_card.dart +++ b/lib/presentation/home/widgets/lakum_wallet_card.dart @@ -92,7 +92,7 @@ class LakumWalletCard extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.end, children: [ (habibWalletVM.isLakumPointsHidden - ? "••••" + ? "****" : NumberFormat.decimalPattern() .format(habibWalletVM.isLakumAccountInfoLoading ? 123 diff --git a/lib/presentation/home/widgets/occasion_banner_widget.dart b/lib/presentation/home/widgets/occasion_banner_widget.dart new file mode 100644 index 00000000..1f19b826 --- /dev/null +++ b/lib/presentation/home/widgets/occasion_banner_widget.dart @@ -0,0 +1,97 @@ +import 'dart:developer'; + +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:hmg_patient_app_new/core/utils/size_utils.dart'; +import 'package:hmg_patient_app_new/extensions/widget_extensions.dart'; +import 'package:hmg_patient_app_new/features/occasion_theme/occasion_theme_view_model.dart'; +import 'package:provider/provider.dart'; + +/// Renders the occasion's main header/banner (`header_image`, e.g. the +/// National Day greeting banner) inline at the top of the landing page's +/// scrollable content, drawn at full opacity. +/// +/// Fully data-driven: when no occasion is active (the default), this widget +/// renders nothing and takes zero space — the existing layout/UI is +/// completely unaffected. +/// +/// See also [OccasionBackgroundWidget], which renders the decorative +/// backdrop image as a full-page background instead of inline content. +class OccasionBannerWidget extends StatelessWidget { + const OccasionBannerWidget({super.key}); + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, occasionThemeVm, _) { + final headerImageUrl = occasionThemeVm.headerImage; + final hasHeaderImage = occasionThemeVm.isOccasionActive && (headerImageUrl?.isNotEmpty ?? false); + + if (!hasHeaderImage) { + return const SizedBox(height: 0); + } + + return buildOccasionImage(headerImageUrl!).paddingOnly(bottom: 16.h); + }, + ); + } +} + +/// Renders the occasion's decorative backdrop/watermark +/// (`background_opacity_image`) as a full-page background. Place this as +/// the first child of the landing page's outer [Stack] (behind the +/// scrollable content) so it fills the whole screen and sits behind +/// everything else, drawn at [OccasionThemeViewModel.backgroundImageOpacity]. +/// +/// Fully data-driven: when no occasion is active (the default), this widget +/// renders nothing — the existing layout/UI is completely unaffected. +class OccasionBackgroundWidget extends StatelessWidget { + const OccasionBackgroundWidget({super.key}); + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, occasionThemeVm, _) { + final backgroundImageUrl = occasionThemeVm.backgroundOpacityImage; + final hasBackgroundImage = occasionThemeVm.isOccasionActive && (backgroundImageUrl?.isNotEmpty ?? false); + + if (!hasBackgroundImage) { + return const SizedBox.shrink(); + } + + return Positioned.fill( + child: Opacity( + opacity: occasionThemeVm.backgroundImageOpacity.clamp(0.0, 1.0), + child: buildOccasionImage(backgroundImageUrl!, isNeedPadding: true), + ), + ); + }, + ); + } +} + +/// Remote-config image URLs may point to either an SVG (e.g. the National +/// Day banner) or a raster image (PNG/JPG). [CachedNetworkImage] cannot +/// decode SVG — it silently renders nothing — so SVG URLs are routed to +/// [SvgPicture.network] instead, based on the URL's file extension. +Widget buildOccasionImage(String url, {bool isNeedPadding = false}) { + final isSvg = url.split('?').first.toLowerCase().endsWith('.svg'); + if (isSvg) { + return SvgPicture.network( + url, + width: double.infinity, + fit: BoxFit.contain, + placeholderBuilder: (_) => const SizedBox.shrink(), + ); + } + return CachedNetworkImage( + imageUrl: url, + width: double.infinity, + fit: BoxFit.contain, + placeholder: (_, __) => const SizedBox.shrink(), + errorWidget: (_, __, error) { + return const SizedBox.shrink(); + }, + ).paddingSymmetrical(isNeedPadding ? 24.w : 0, 0); +} diff --git a/lib/presentation/home/widgets/small_service_card.dart b/lib/presentation/home/widgets/small_service_card.dart index 45023c3b..3c5ebbf7 100644 --- a/lib/presentation/home/widgets/small_service_card.dart +++ b/lib/presentation/home/widgets/small_service_card.dart @@ -38,7 +38,7 @@ class SmallServiceCard extends StatelessWidget { final Color? backgroundColor; final bool isBold; - SmallServiceCard({ + const SmallServiceCard({ super.key, this.icon = "", this.serviceName = "", diff --git a/lib/presentation/insurance/widgets/insurance_history.dart b/lib/presentation/insurance/widgets/insurance_history.dart index 0f5bde1e..f2019c91 100644 --- a/lib/presentation/insurance/widgets/insurance_history.dart +++ b/lib/presentation/insurance/widgets/insurance_history.dart @@ -69,8 +69,8 @@ class InsuranceHistory extends StatelessWidget { CustomButton( text: insuranceVM.patientInsuranceCardHistoryList[index].statusDescription ?? "", onPressed: () {}, - backgroundColor: AppColors.primaryRedColor.withOpacity(0.1), - borderColor: AppColors.primaryRedColor.withOpacity(0.0), + backgroundColor: AppColors.secondaryLightRedColor, + borderColor: Colors.transparent, textColor: AppColors.primaryRedColor, fontSize: 10, isBold: true, diff --git a/lib/presentation/insurance/widgets/patient_insurance_card.dart b/lib/presentation/insurance/widgets/patient_insurance_card.dart index 6fab990c..87ae3a97 100644 --- a/lib/presentation/insurance/widgets/patient_insurance_card.dart +++ b/lib/presentation/insurance/widgets/patient_insurance_card.dart @@ -117,7 +117,7 @@ class PatientInsuranceCard extends StatelessWidget { } }, backgroundColor: isCurrentPatientInsuranceExpired(insuranceCardDetailsModel.cardValidTo!) - ? AppColors.primaryRedColor.withOpacity(0.1) + ? AppColors.secondaryLightRedColor : insuranceViewModel.isInsuranceActive ? AppColors.successColor.withOpacity(0.1) : AppColors.warningColorYellow.withOpacity(0.1), @@ -129,7 +129,7 @@ class PatientInsuranceCard extends StatelessWidget { // iconColor: isInsuranceExpired ? AppColors.primaryRedColor : AppColors.successColor, // textColor: isInsuranceExpired ? AppColors.primaryRedColor : AppColors.successColor, // iconSize: 12, - // backgroundColor: isInsuranceExpired ? AppColors.primaryRedColor.withOpacity(0.1) : AppColors.successColor.withOpacity(0.1), + // backgroundColor: isInsuranceExpired ? AppColors.primaryRedColorBase.withOpacity(0.1) : AppColors.successColor.withOpacity(0.1), // labelPadding: EdgeInsetsDirectional.only(start: -4.h, end: 8.h), // ), ], diff --git a/lib/presentation/lab/alphabeticScroll.dart b/lib/presentation/lab/alphabeticScroll.dart index ff0efbf4..a1a9add8 100644 --- a/lib/presentation/lab/alphabeticScroll.dart +++ b/lib/presentation/lab/alphabeticScroll.dart @@ -14,8 +14,8 @@ import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; import 'lab_order_by_test.dart'; -class AlphabeticScroll extends StatefulWidget{ - final List alpahbetsAvailable; +class AlphabeticScroll extends StatefulWidget { + final List alpahbetsAvailable; final List details; final AppState appState; final LabViewModel labViewModel; @@ -38,13 +38,13 @@ class _AlphabetScrollPageState extends State { Map density = {}; Map _offsetMap = {}; - Map _offsetKeys = {}; + Map _offsetKeys = {}; int _activeIndex = 0; // <-- Highlighted letter @override void initState() { super.initState(); - scheduleMicrotask((){ + scheduleMicrotask(() { _rebuildData(); }); itemPositionsListener.itemPositions.addListener(_onPositionChanged); @@ -61,9 +61,7 @@ class _AlphabetScrollPageState extends State { void _rebuildData() { data.clear(); for (var char in widget.alpahbetsAvailable) { - data[char] = widget.details - .where((element) => element.description?.toLowerCase().startsWith(char.toLowerCase()) == true) - .toList(); + data[char] = widget.details.where((element) => element.description?.toLowerCase().startsWith(char.toLowerCase()) == true).toList(); } if (_activeIndex >= widget.alpahbetsAvailable.length) { _activeIndex = 0; @@ -79,15 +77,12 @@ class _AlphabetScrollPageState extends State { // Get FIRST visible item (top-most) final firstVisible = positions .where((p) => p.itemTrailingEdge > 0) // visible - .reduce((min, p) => - p.itemLeadingEdge < min.itemLeadingEdge ? p : min); + .reduce((min, p) => p.itemLeadingEdge < min.itemLeadingEdge ? p : min); - if(_activeIndex == firstVisible.index) return ; + if (_activeIndex == firstVisible.index) return; setState(() { _activeIndex = firstVisible.index; }); - - print("Active index = $_activeIndex"); } @override @@ -97,12 +92,8 @@ class _AlphabetScrollPageState extends State { } void _scrollToLetter(String letter) async { - // itemScrollController.jumpTo(index:density[letter]?.toInt()??0, ); - itemScrollController.scrollTo( - index: data.keys.toList().indexOf(letter).toInt()??0, - duration: Duration(seconds: 2), - curve: Curves.easeInOutCubic); + itemScrollController.scrollTo(index: data.keys.toList().indexOf(letter).toInt() ?? 0, duration: Duration(seconds: 2), curve: Curves.easeInOutCubic); // final key = _offsetKeys[letter]; // if (key == null) return; // @@ -134,58 +125,52 @@ class _AlphabetScrollPageState extends State { @override Widget build(BuildContext context) { return SizedBox( - width: MediaQuery.sizeOf(context).width, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, // Add this - children: [ - Expanded( - child: Column( - children: [ - SizedBox( - height: (MediaQuery.sizeOf(context).height), - child: _buildList(widget.alpahbetsAvailable) - ), - ], - ), + width: MediaQuery.sizeOf(context).width, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, // Add this + children: [ + Expanded( + child: Column( + children: [ + SizedBox(height: (MediaQuery.sizeOf(context).height), child: _buildList(widget.alpahbetsAvailable)), + ], ), - SizedBox( - width: 24.w, - height: MediaQuery.sizeOf(context).height-(70.h+ kToolbarHeight+100.h), - child: Column( - mainAxisSize: MainAxisSize.max, // Add this - mainAxisAlignment: MainAxisAlignment.center, // Changed from center to start - crossAxisAlignment: CrossAxisAlignment.center, - children:List.generate(widget.alpahbetsAvailable.length, (i) { - final isActive = (i == _activeIndex); - return GestureDetector( - onTap: () { - setState(() => _activeIndex = i); - _scrollToLetter(widget.alpahbetsAvailable[i]); - }, - child: TweenAnimationBuilder( - tween: Tween(begin: 1.0, end: isActive ? 1.8 : 1.0), - duration: Duration(milliseconds: 120), - curve: Curves.easeOut, - builder: (_, scale, child) { - return Transform.scale( - scale: scale, - child: Opacity( - opacity: isActive ? 1.0 : 0.5, - child: widget.alpahbetsAvailable[i].toUpperCase().toText14( - color: !isActive ? AppColors.greyTextColor : AppColors.primaryRedColor, isEnglishOnly: true - ), - ), - ); - }, - ), - ); - } + ), + SizedBox( + width: 24.w, + height: MediaQuery.sizeOf(context).height - (70.h + kToolbarHeight + 100.h), + child: Column( + mainAxisSize: MainAxisSize.max, // Add this + mainAxisAlignment: MainAxisAlignment.center, // Changed from center to start + crossAxisAlignment: CrossAxisAlignment.center, + children: List.generate(widget.alpahbetsAvailable.length, (i) { + final isActive = (i == _activeIndex); + return GestureDetector( + onTap: () { + setState(() => _activeIndex = i); + _scrollToLetter(widget.alpahbetsAvailable[i]); + }, + child: TweenAnimationBuilder( + tween: Tween(begin: 1.0, end: isActive ? 1.8 : 1.0), + duration: Duration(milliseconds: 120), + curve: Curves.easeOut, + builder: (_, scale, child) { + return Transform.scale( + scale: scale, + child: Opacity( + opacity: isActive ? 1.0 : 0.5, + child: widget.alpahbetsAvailable[i].toUpperCase().toText14(color: !isActive ? AppColors.greyTextColor : AppColors.primaryRedColor, isEnglishOnly: true), + ), + ); + }, ), - ), - ), - ], - ), - ); + ); + }), + ), + ), + ], + ), + ); // ); } @@ -205,35 +190,35 @@ class _AlphabetScrollPageState extends State { key: _offsetKeys[letter], child: Column( crossAxisAlignment: CrossAxisAlignment.start, - children: - List.generate(items.length,(itemIndex)=> - AnimationConfiguration.staggeredList( - position: index, - duration: const Duration(milliseconds: 500), - child: SlideAnimation( - verticalOffset: 100.0, - child: FadeInAnimation( - child: LabOrderByTest( - appState: getIt(), - onTap: () { - if (items[itemIndex].model != null) { - widget.rangeViewModel.flush(); - widget.labViewModel.getPatientLabResult(items[itemIndex].model!, items[itemIndex].description!, - (widget.appState.isArabic() ? items[itemIndex].testDescriptionAr! : items[itemIndex].testDescriptionEn!),""); - } - }, - tests: items[itemIndex], - index: itemIndex, - isExpanded: true)), - ), - )) + children: List.generate( + items.length, + (itemIndex) => AnimationConfiguration.staggeredList( + position: index, + duration: const Duration(milliseconds: 500), + child: SlideAnimation( + verticalOffset: 100.0, + child: FadeInAnimation( + child: LabOrderByTest( + appState: getIt(), + onTap: () { + if (items[itemIndex].model != null) { + widget.rangeViewModel.flush(); + widget.labViewModel.getPatientLabResult(items[itemIndex].model!, items[itemIndex].description!, + (widget.appState.isArabic() ? items[itemIndex].testDescriptionAr! : items[itemIndex].testDescriptionEn!), ""); + } + }, + tests: items[itemIndex], + index: itemIndex, + isExpanded: true)), + ), + )) // ...items.indexed((item) => // // // // ) - ), + ), ); }, ); diff --git a/lib/presentation/lab/lab_order_by_test.dart b/lib/presentation/lab/lab_order_by_test.dart index 7740a596..733fb792 100644 --- a/lib/presentation/lab/lab_order_by_test.dart +++ b/lib/presentation/lab/lab_order_by_test.dart @@ -26,64 +26,65 @@ class LabOrderByTest extends StatelessWidget { @override build(BuildContext context) { return AnimatedContainer( - key:key, - 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: Container( - key: ValueKey(index), - padding: EdgeInsets.symmetric(horizontal: 16.h, vertical: 8.h), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - '${tests!.description}'.toText16(isBold: true, isEnglishOnly: true), - SizedBox(height: 4.h), - (appState.isArabic() ? tests!.testDescriptionAr : tests!.testDescriptionEn)!.toText12(isBold: true), - SizedBox(height: 8.h), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - AppCustomChipWidget( - richText: Row( - children: [ - "${LocaleKeys.lastTested.tr(context: context)}: ".toText12(isBold: true), - Directionality( - textDirection: ui.TextDirection.ltr,child: DateUtil.formatDateToDate(DateUtil.convertStringToDate(tests!.createdOn), false).toText12(isBold: true, isEnglishOnly: true)) - ], - ), - backgroundColor: AppColors.greyLightColor, - textColor: AppColors.textColor, + key: key, + 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: Container( + key: ValueKey(index), + padding: EdgeInsets.symmetric(horizontal: 16.h, vertical: 8.h), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + '${tests!.description}'.toText16(isBold: true, isEnglishOnly: true), + SizedBox(height: 4.h), + (appState.isArabic() ? tests!.testDescriptionAr : tests!.testDescriptionEn)!.toText12(isBold: true), + SizedBox(height: 8.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + AppCustomChipWidget( + richText: Row( + children: [ + "${LocaleKeys.lastTested.tr(context: context)}: ".toText12(isBold: true), + Directionality( + textDirection: ui.TextDirection.ltr, child: DateUtil.formatDateToDate(DateUtil.convertStringToDate(tests!.createdOn), false).toText12(isBold: true, isEnglishOnly: true)) + ], ), - ], - ), - SizedBox(height: 16.h), - Row( - children: [ - Expanded(child: Container()), - Expanded( - child: CustomButton( - icon: AppAssets.view_report_icon, - iconColor: AppColors.primaryRedColor, - iconSize: 16.h, - text: LocaleKeys.viewResults.tr(context: context), - onPressed: () { - onTap(); - }, - backgroundColor: AppColors.secondaryLightRedColor, - borderColor: AppColors.secondaryLightRedColor, - textColor: AppColors.primaryRedColor, - fontSize: 14.f, - fontWeight: FontWeight.w600, - borderRadius: 12, - padding: EdgeInsets.fromLTRB(10, 0, 10, 0), - height: 40.h, - ), + backgroundColor: AppColors.greyLightColor, + textColor: AppColors.textColor, + ), + ], + ), + SizedBox(height: 16.h), + Row( + children: [ + Expanded(child: Container()), + Expanded( + child: CustomButton( + icon: AppAssets.view_report_icon, + iconColor: AppColors.primaryRedColor, + iconSize: 16.h, + text: LocaleKeys.viewResults.tr(context: context), + onPressed: () { + onTap(); + }, + backgroundColor: AppColors.secondaryLightRedColor, + borderColor: Colors.transparent, + textColor: AppColors.primaryRedColor, + fontSize: 14.f, + fontWeight: FontWeight.w600, + borderRadius: 10.r, + padding: EdgeInsets.fromLTRB(10, 0, 10, 0), + height: 40.h, ), - ], - ), - ], - ), - )); + ), + ], + ), + ], + ), + ), + ); } } diff --git a/lib/presentation/lab/lab_orders_page.dart b/lib/presentation/lab/lab_orders_page.dart index 0e8890ba..332a89da 100644 --- a/lib/presentation/lab/lab_orders_page.dart +++ b/lib/presentation/lab/lab_orders_page.dart @@ -1 +1 @@ -import 'dart:async'; import 'dart:ui' as ui; 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/app_state.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/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/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'; 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'; 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), physics: activeIndex == 1 ? NeverScrollableScrollPhysics() : ScrollPhysics(), 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()), ], 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, // isBold: true, // 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, // isBold: true, // 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( richText: Row( children: [ "${group.testDetails!.length} ".toText10( color: AppColors.successColor, isEnglishOnly: true, weight: FontWeight.w600), LocaleKeys.tests .tr(context: context) .toText10(color: AppColors.successColor, weight: FontWeight.w600), ], ), 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(isBold: true)), ], ), 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(isBold: true, isEnglishOnly: true), SizedBox(height: 4.h), (_appState.isArabic() ? order.testDetails![index].testDescriptionAr! : order.testDetails![index].testDescriptionEn!).toText12( isBold: true, color: AppColors.textColorLight, isEnglishOnly: true), ], ).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, isBold: true, 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 +import 'dart:async'; import 'dart:ui' as ui; 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/app_state.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/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/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'; 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'; 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), physics: activeIndex == 1 ? NeverScrollableScrollPhysics() : ScrollPhysics(), 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: AppColors.primaryRedColor, activeBackgroundColor: AppColors.secondaryLightRedColor, tabs: [ CustomTabBarModel(null, LocaleKeys.byVisit.tr()), CustomTabBarModel(null, LocaleKeys.byTest.tr()), ], 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, // isBold: true, // 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, // isBold: true, // 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( richText: Row( children: [ "${group.testDetails!.length} ".toText10(color: AppColors.successColor, isEnglishOnly: true, weight: FontWeight.w600), LocaleKeys.tests.tr(context: context).toText10(color: AppColors.successColor, weight: FontWeight.w600), ], ), 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(isBold: true)), ], ), 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(isBold: true, isEnglishOnly: true), SizedBox(height: 4.h), (_appState.isArabic() ? order.testDetails![index].testDescriptionAr! : order.testDetails![index].testDescriptionEn!) .toText12(isBold: true, color: AppColors.textColorLight, isEnglishOnly: true), ], ).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, isBold: true, 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/medical_file/medical_file_page.dart b/lib/presentation/medical_file/medical_file_page.dart index 27978723..629ff15c 100644 --- a/lib/presentation/medical_file/medical_file_page.dart +++ b/lib/presentation/medical_file/medical_file_page.dart @@ -340,7 +340,7 @@ class _MedicalFilePageState extends State { } }, backgroundColor: insuranceVM.isInsuranceExpired - ? AppColors.primaryRedColor.withOpacity(0.1) + ? AppColors.secondaryLightRedColor : insuranceVM.isInsuranceActive ? AppColors.successColor.withOpacity(0.1) : AppColors.warningColorYellow.withOpacity(0.1), diff --git a/lib/presentation/my_family/my_family.dart b/lib/presentation/my_family/my_family.dart index 4b6f820a..76f318f7 100644 --- a/lib/presentation/my_family/my_family.dart +++ b/lib/presentation/my_family/my_family.dart @@ -34,14 +34,13 @@ class _FamilyMedicalScreenState extends State { super.initState(); medicalVM = context.read(); WidgetsBinding.instance.addPostFrameCallback((_) async { - if(medicalVM!.isFamilyFilesLoading) { + if (medicalVM!.isFamilyFilesLoading) { LoaderBottomSheet.showLoader(); } else { LoaderBottomSheet.hideLoader(); } medicalVM?.onFamilyFileTabChange(0); medicalVM?.getAllPendingRecordsByResponseId(); - }); } @@ -71,6 +70,8 @@ class _FamilyMedicalScreenState extends State { medicalVM!.addFamilyFile(otpTypeEnum: OTPTypeEnum.sms); }); }, + borderColor: AppColors.primaryRedColor, + backgroundColor: AppColors.primaryRedColor, icon: AppAssets.add_icon, fontSize: 16.f, borderRadius: 12.r, @@ -104,9 +105,7 @@ class _FamilyMedicalScreenState extends State { ), shouldRebuild: (previous, next) { // Only rebuild if something actually changed - return previous.selectedIndex != next.selectedIndex || - !identical(previous.patientFiles, next.patientFiles) || - !identical(previous.pendingFiles, next.pendingFiles); + return previous.selectedIndex != next.selectedIndex || !identical(previous.patientFiles, next.patientFiles) || !identical(previous.pendingFiles, next.pendingFiles); }, builder: (context, data, child) => getFamilyTabs( index: data.selectedIndex, @@ -127,7 +126,6 @@ class _FamilyMedicalScreenState extends State { }) { switch (index) { case 0: - return FamilyCards( profiles: patientFiles, onSelect: (FamilyFileResponseModelLists profile) { diff --git a/lib/presentation/my_family/widget/family_cards.dart b/lib/presentation/my_family/widget/family_cards.dart index 0567e451..a52f2ca6 100644 --- a/lib/presentation/my_family/widget/family_cards.dart +++ b/lib/presentation/my_family/widget/family_cards.dart @@ -165,67 +165,63 @@ class _FamilyCardsState extends State { title: LocaleKeys.whoCanViewMyMedicalFile.tr(context: context).toText18(isBold: true), expandedBackgroundColor: Colors.transparent, initiallyExpanded: true, - children: [ - SizedBox(height: 10.h), - manageFamily() - ], + children: [SizedBox(height: 10.h), manageFamily()], ), ExpandableListItem(title: LocaleKeys.notifications.tr(context: context).toText18(isBold: true), expandedBackgroundColor: Colors.transparent, initiallyExpanded: true, children: [ SizedBox(height: 10.h), widget.profileViewList!.where((profile) => profile.isRequestFromMySide ?? false).isEmpty ? Utils.getNoDataWidget(context) : ListView.builder( - shrinkWrap: true, - physics: NeverScrollableScrollPhysics(), - padding: EdgeInsets.zero, - itemCount: widget.profileViewList!.where((profile) => profile.isRequestFromMySide ?? false).length, - itemBuilder: (context, index) { - final mySideProfiles = widget.profileViewList!.where((profile) => profile.isRequestFromMySide ?? false).toList(); - FamilyFileResponseModelLists profile = mySideProfiles[index]; - return Container( - margin: EdgeInsets.only(bottom: 12.h), - padding: EdgeInsets.symmetric(vertical: 15.h, horizontal: 15.h), - decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24), - child: Opacity( - opacity: 1.0, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - AppCustomChipWidget( - labelText: profile.statusDescription ?? " N/A", - backgroundColor: profile.status == FamilyFileEnum.pending.toInt - ? AppColors.alertLightColor.withValues(alpha: 0.20) - : profile.status == FamilyFileEnum.rejected.toInt - ? AppColors.primaryRedColor.withValues(alpha: 0.20) - : profile.status == FamilyFileEnum.active.toInt - ? AppColors.lightGreenColor - : AppColors.lightGrayBGColor, - textColor: profile.status == FamilyFileEnum.pending.toInt - ? AppColors.alertLightColor - : profile.status == FamilyFileEnum.rejected.toInt - ? AppColors.primaryRedColor - : profile.status == FamilyFileEnum.active.toInt - ? AppColors.textGreenColor - : AppColors.alertColor - ), - SizedBox(height: 8.h), - Wrap(alignment: WrapAlignment.start, crossAxisAlignment: WrapCrossAlignment.start, runAlignment: WrapAlignment.start, spacing: 0.h, children: [ - (profile.patientName ?? "").toText14(isBold: true, isCenter: false, maxlines: 1), - (getStatusTextByRequest(FamilyFileEnum.values.firstWhere((e) => e.toInt == profile.status), profile.isRequestFromMySide ?? false)) - .toText12(isCenter: false, maxLine: 1, isBold: true, color: AppColors.greyTextColor), - ]), - SizedBox(height: 8.h), - AppCustomChipWidget( - labelText: "Medical File: ${profile.responseId ?? "N/A"}", + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + padding: EdgeInsets.zero, + itemCount: widget.profileViewList!.where((profile) => profile.isRequestFromMySide ?? false).length, + itemBuilder: (context, index) { + final mySideProfiles = widget.profileViewList!.where((profile) => profile.isRequestFromMySide ?? false).toList(); + FamilyFileResponseModelLists profile = mySideProfiles[index]; + return Container( + margin: EdgeInsets.only(bottom: 12.h), + padding: EdgeInsets.symmetric(vertical: 15.h, horizontal: 15.h), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24), + child: Opacity( + opacity: 1.0, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + AppCustomChipWidget( + labelText: profile.statusDescription ?? " N/A", + backgroundColor: profile.status == FamilyFileEnum.pending.toInt + ? AppColors.alertLightColor.withValues(alpha: 0.20) + : profile.status == FamilyFileEnum.rejected.toInt + ? AppColors.primaryRedColor.withValues(alpha: 0.20) + : profile.status == FamilyFileEnum.active.toInt + ? AppColors.lightGreenColor + : AppColors.lightGrayBGColor, + textColor: profile.status == FamilyFileEnum.pending.toInt + ? AppColors.alertLightColor + : profile.status == FamilyFileEnum.rejected.toInt + ? AppColors.primaryRedColor + : profile.status == FamilyFileEnum.active.toInt + ? AppColors.textGreenColor + : AppColors.alertColor), + SizedBox(height: 8.h), + Wrap(alignment: WrapAlignment.start, crossAxisAlignment: WrapCrossAlignment.start, runAlignment: WrapAlignment.start, spacing: 0.h, children: [ + (profile.patientName ?? "").toText14(isBold: true, isCenter: false, maxlines: 1), + (getStatusTextByRequest(FamilyFileEnum.values.firstWhere((e) => e.toInt == profile.status), profile.isRequestFromMySide ?? false)) + .toText12(isCenter: false, maxLine: 1, isBold: true, color: AppColors.greyTextColor), + ]), + SizedBox(height: 8.h), + AppCustomChipWidget( + labelText: "Medical File: ${profile.responseId ?? "N/A"}", + ), + ], + ), ), - ], - ), + ); + }, ), - ); - }, - ), ]) ], theme: ExpandableListTheme.custom( @@ -517,7 +513,7 @@ class _FamilyCardsState extends State { }, text: isActive ? LocaleKeys.active.tr(context: context) : LocaleKeys.switchLogin.tr(context: context), backgroundColor: isActive || !canSwitch ? Colors.grey.shade200 : AppColors.secondaryLightRedColor, - borderColor: isActive || !canSwitch ? Colors.grey.shade200 : AppColors.secondaryLightRedColor, + borderColor: Colors.transparent, textColor: isActive || !canSwitch ? AppColors.greyTextColor : AppColors.primaryRedColor, fontSize: 13.h, icon: isActive ? AppAssets.activeCheck : AppAssets.switch_user, @@ -527,13 +523,15 @@ class _FamilyCardsState extends State { ], ), if (widget.isShowRemoveButton) ...[ - isParentUser ? Positioned( - top: 0, - right: 0, - child: Utils.buildSvgWithAssets(icon: AppAssets.deleteIcon).onPress(() { - if (!isActive) widget.onRemove(profile); - }), - ) : SizedBox(), + isParentUser + ? Positioned( + top: 0, + right: 0, + child: Utils.buildSvgWithAssets(icon: AppAssets.deleteIcon).onPress(() { + if (!isActive) widget.onRemove(profile); + }), + ) + : SizedBox(), ], ], ), diff --git a/lib/presentation/my_invoices/my_invoices_list.dart b/lib/presentation/my_invoices/my_invoices_list.dart index 67ee05a6..95ff3e16 100644 --- a/lib/presentation/my_invoices/my_invoices_list.dart +++ b/lib/presentation/my_invoices/my_invoices_list.dart @@ -122,8 +122,8 @@ class _MyInvoicesListState extends State { children: [ SizedBox(height: 16.h), CustomTabBar( - activeTextColor: Color(0xffED1C2B), - activeBackgroundColor: Color(0xffED1C2B).withValues(alpha: .1), + activeTextColor: AppColors.primaryRedColor, + activeBackgroundColor: AppColors.secondaryLightRedColor, initialIndex: 0, tabs: [ CustomTabBarModel(null, LocaleKeys.visitsOrders.tr(context: context)), @@ -149,13 +149,11 @@ class _MyInvoicesListState extends State { myInvoicesViewModel.filterInvoices(InvoiceFilterType.all); }, backgroundColor: myInvoicesVM.currentFilter == InvoiceFilterType.all ? AppColors.bgRedLightColor : AppColors.whiteColor, - borderColor: myInvoicesVM.currentFilter == InvoiceFilterType.all - ? AppColors.primaryRedColor - : AppColors.textColor.withValues(alpha: 0.2), + borderColor: myInvoicesVM.currentFilter == InvoiceFilterType.all ? AppColors.primaryRedColor : AppColors.textColor.withValues(alpha: 0.2), textColor: myInvoicesVM.currentFilter == InvoiceFilterType.all ? AppColors.primaryRedColor : AppColors.blackColor, - fontSize: 12, + fontSize: 12.f, fontWeight: FontWeight.w600, - borderRadius: 10, + borderRadius: 10.r, padding: EdgeInsets.fromLTRB(10, 0, 10, 0), height: 40.h, ), @@ -166,15 +164,12 @@ class _MyInvoicesListState extends State { if (myInvoicesVM.getOriginalInvoicesList().isEmpty) return; _showHospitalFilterBottomSheet(context); }, - backgroundColor: - myInvoicesVM.currentFilter == InvoiceFilterType.hospital ? AppColors.bgRedLightColor : AppColors.whiteColor, - borderColor: myInvoicesVM.currentFilter == InvoiceFilterType.hospital - ? AppColors.primaryRedColor - : AppColors.textColor.withValues(alpha: 0.2), + backgroundColor: myInvoicesVM.currentFilter == InvoiceFilterType.hospital ? AppColors.bgRedLightColor : AppColors.whiteColor, + borderColor: myInvoicesVM.currentFilter == InvoiceFilterType.hospital ? AppColors.primaryRedColor : AppColors.textColor.withValues(alpha: 0.2), textColor: myInvoicesVM.currentFilter == InvoiceFilterType.hospital ? AppColors.primaryRedColor : AppColors.blackColor, - fontSize: 12, + fontSize: 12.f, fontWeight: FontWeight.w600, - borderRadius: 10, + borderRadius: 10.r, padding: EdgeInsets.fromLTRB(10, 0, 10, 0), height: 40.h, ), @@ -185,15 +180,12 @@ class _MyInvoicesListState extends State { if (myInvoicesVM.getOriginalInvoicesList().isEmpty) return; _showClinicFilterBottomSheet(context); }, - backgroundColor: - myInvoicesVM.currentFilter == InvoiceFilterType.clinic ? AppColors.bgRedLightColor : AppColors.whiteColor, - borderColor: myInvoicesVM.currentFilter == InvoiceFilterType.clinic - ? AppColors.primaryRedColor - : AppColors.textColor.withValues(alpha: 0.2), + backgroundColor: myInvoicesVM.currentFilter == InvoiceFilterType.clinic ? AppColors.bgRedLightColor : AppColors.whiteColor, + borderColor: myInvoicesVM.currentFilter == InvoiceFilterType.clinic ? AppColors.primaryRedColor : AppColors.textColor.withValues(alpha: 0.2), textColor: myInvoicesVM.currentFilter == InvoiceFilterType.clinic ? AppColors.primaryRedColor : AppColors.blackColor, - fontSize: 12, + fontSize: 12.f, fontWeight: FontWeight.w600, - borderRadius: 10, + borderRadius: 10.r, padding: EdgeInsets.fromLTRB(10, 0, 10, 0), height: 40.h, ), @@ -204,15 +196,12 @@ class _MyInvoicesListState extends State { if (myInvoicesVM.getOriginalInvoicesList().isEmpty) return; _showDoctorFilterBottomSheet(context); }, - backgroundColor: - myInvoicesVM.currentFilter == InvoiceFilterType.doctor ? AppColors.bgRedLightColor : AppColors.whiteColor, - borderColor: myInvoicesVM.currentFilter == InvoiceFilterType.doctor - ? AppColors.primaryRedColor - : AppColors.textColor.withValues(alpha: 0.2), + backgroundColor: myInvoicesVM.currentFilter == InvoiceFilterType.doctor ? AppColors.bgRedLightColor : AppColors.whiteColor, + borderColor: myInvoicesVM.currentFilter == InvoiceFilterType.doctor ? AppColors.primaryRedColor : AppColors.textColor.withValues(alpha: 0.2), textColor: myInvoicesVM.currentFilter == InvoiceFilterType.doctor ? AppColors.primaryRedColor : AppColors.blackColor, - fontSize: 12, + fontSize: 12.f, fontWeight: FontWeight.w600, - borderRadius: 10, + borderRadius: 10.r, padding: EdgeInsets.fromLTRB(10, 0, 10, 0), height: 40.h, ), @@ -245,16 +234,14 @@ class _MyInvoicesListState extends State { getInvoicesListResponseModel: myInvoicesVM.allInvoicesList[index], onTap: () async { if (Utils.isVidaPlusProject(myInvoicesVM.allInvoicesList[index].projectId ?? 0)) { - LoaderBottomSheet.showLoader( - loadingText: LocaleKeys.sendingEmailPleaseWait.tr(context: context)); + LoaderBottomSheet.showLoader(loadingText: LocaleKeys.sendingEmailPleaseWait.tr(context: context)); await myInvoicesViewModel.sendInvoiceEmail( appointmentNo: myInvoicesVM.allInvoicesList[index].appointmentNo!, projectID: myInvoicesVM.allInvoicesList[index].projectId!, onSuccess: (val) { LoaderBottomSheet.hideLoader(); showCommonBottomSheetWithoutHeight(context, - child: Utils.getSuccessWidget( - loadingText: LocaleKeys.emailSentSuccessfullyMessage.tr(context: context)), + child: Utils.getSuccessWidget(loadingText: LocaleKeys.emailSentSuccessfullyMessage.tr(context: context)), callBackFunc: () {}, isFullScreen: false, isCloseButtonVisible: true, @@ -289,8 +276,7 @@ class _MyInvoicesListState extends State { onSuccess: (value) async { LoaderBottomSheet.hideLoader(); if (myInvoicesViewModel.downloadInvoicePDFBase64!.isNotEmpty) { - String path = await Utils.createFileFromString( - myInvoicesViewModel.downloadInvoicePDFBase64!, "pdf"); + String path = await Utils.createFileFromString(myInvoicesViewModel.downloadInvoicePDFBase64!, "pdf"); try { OpenFilex.open(path); } catch (ex) { @@ -377,8 +363,7 @@ class _MyInvoicesListState extends State { onSuccess: (value) async { LoaderBottomSheet.hideLoader(); if (myInvoicesViewModel.downloadPharmacyInvoicePDFBase64!.isNotEmpty) { - String path = await Utils.createFileFromString( - myInvoicesViewModel.downloadPharmacyInvoicePDFBase64!, "pdf"); + String path = await Utils.createFileFromString(myInvoicesViewModel.downloadPharmacyInvoicePDFBase64!, "pdf"); try { OpenFilex.open(path); } catch (ex) { diff --git a/lib/presentation/onboarding/onboarding_screen.dart b/lib/presentation/onboarding/onboarding_screen.dart index 0af63b40..1c5b64d8 100644 --- a/lib/presentation/onboarding/onboarding_screen.dart +++ b/lib/presentation/onboarding/onboarding_screen.dart @@ -111,7 +111,7 @@ class _OnboardingScreenState extends State { onPressed: () => goToHomePage(), width: 86.w, height: 56.h, - backgroundColor: Color(0xffFEE9EA), + backgroundColor: AppColors.secondaryLightRedColor, textColor: AppColors.primaryRedColor, borderColor: Colors.transparent, ).paddingOnly(left: 24.w) diff --git a/lib/presentation/onboarding/splash_animation_screen.dart b/lib/presentation/onboarding/splash_animation_screen.dart index 105922bb..0fdd86a8 100644 --- a/lib/presentation/onboarding/splash_animation_screen.dart +++ b/lib/presentation/onboarding/splash_animation_screen.dart @@ -164,7 +164,7 @@ class _AnimatedScreenState extends State with TickerProviderStat width: 400.h, height: 653.h, decoration: BoxDecoration( - color: Color(0xffED1C2B), + color: AppColors.primaryRedColor, borderRadius: BorderRadius.circular(330.h), ), ), @@ -273,7 +273,7 @@ class _MoveObjectDemoState extends State with SingleTickerProvid width: 200, height: 375, decoration: BoxDecoration( - color: const Color(0xffED1C2B), + color: AppColors.primaryRedColor, borderRadius: BorderRadius.circular(330), ), ), @@ -367,7 +367,7 @@ class _MoveOnClickDemoState extends State with TickerProviderSt width: 100, height: 150, decoration: BoxDecoration( - color: const Color(0xffED1C2B), + color: AppColors.primaryRedColor, borderRadius: BorderRadius.circular(330), ), ), diff --git a/lib/presentation/prescriptions/prescription_item_view.dart b/lib/presentation/prescriptions/prescription_item_view.dart index b0e11af0..59b10620 100644 --- a/lib/presentation/prescriptions/prescription_item_view.dart +++ b/lib/presentation/prescriptions/prescription_item_view.dart @@ -177,8 +177,8 @@ class PrescriptionItemView extends StatelessWidget { // child: CustomButton( // text: LocaleKeys.checkAvailability.tr(context: context), // onPressed: () {}, - // backgroundColor: AppColors.primaryRedColor.withOpacity(0.1), - // borderColor: AppColors.primaryRedColor.withOpacity(0.0), + // backgroundColor: AppColors.primaryRedColorBase.withOpacity(0.1), + // borderColor: AppColors.primaryRedColorBase.withOpacity(0.0), // textColor: AppColors.primaryRedColor, // fontSize: 14.f, // isBold: true, diff --git a/lib/presentation/profile_settings/profile_settings.dart b/lib/presentation/profile_settings/profile_settings.dart index 2e9bb3b7..ce300c9b 100644 --- a/lib/presentation/profile_settings/profile_settings.dart +++ b/lib/presentation/profile_settings/profile_settings.dart @@ -52,14 +52,13 @@ class ProfileSettings extends StatefulWidget { } class ProfileSettingsState extends State { - final InAppReview inAppReview = InAppReview.instance; @override void initState() { super.initState(); _loadPermissions(); - + // Load data after first frame WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) { @@ -434,7 +433,15 @@ class ProfileSettingsState extends State { if (trailingLabel.isNotEmpty) ConstrainedBox( constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.35), - child: isEnglishOnly ? Directionality(textDirection: ui.TextDirection.ltr, child: trailingLabel.toText14(color: AppColors.greyTextColor, isBold: true, textOverflow: TextOverflow.ellipsis, maxlines: 1, isEnglishOnly: isEnglishOnly).toShimmer2(isShow: isShowLoading)) : trailingLabel.toText14(color: AppColors.greyTextColor, isBold: true, textOverflow: TextOverflow.ellipsis, maxlines: 1, isEnglishOnly: isEnglishOnly).toShimmer2(isShow: isShowLoading), + child: isEnglishOnly + ? Directionality( + textDirection: ui.TextDirection.ltr, + child: trailingLabel + .toText14(color: AppColors.greyTextColor, isBold: true, textOverflow: TextOverflow.ellipsis, maxlines: 1, isEnglishOnly: isEnglishOnly) + .toShimmer2(isShow: isShowLoading)) + : trailingLabel + .toText14(color: AppColors.greyTextColor, isBold: true, textOverflow: TextOverflow.ellipsis, maxlines: 1, isEnglishOnly: isEnglishOnly) + .toShimmer2(isShow: isShowLoading), ), switchValue != null ? Switch( @@ -452,8 +459,10 @@ class ProfileSettingsState extends State { ); } - Widget socialMedia(String icon, String label, - ) { + Widget socialMedia( + String icon, + String label, + ) { return SizedBox( height: 56.h, child: Row( @@ -461,14 +470,13 @@ class ProfileSettingsState extends State { children: [ Utils.buildSvgWithAssets(icon: icon, iconColor: AppColors.greyTextColor), label.toText14(isBold: true, textOverflow: TextOverflow.ellipsis, maxlines: 1).expanded, - Row( spacing: 24.w, children: [ - Utils.buildSvgWithAssets(icon: AppAssets.x2).onPress(()=> launchUrl(Uri.parse("https://twitter.com/HMG"))), - Utils.buildSvgWithAssets(icon: AppAssets.fb).onPress(()=> launchUrl(Uri.parse("https://www.facebook.com/DrSulaimanAlHabib?ref=tn_tnmn"))), - Utils.buildSvgWithAssets(icon: AppAssets.linkedin).onPress(()=> launchUrl(Uri.parse("https://www.linkedin.com/company/drsulaiman-alhabib-medical-group"))), - Utils.buildSvgWithAssets(icon: AppAssets.youtube).onPress(()=> launchUrl(Uri.parse("https://www.youtube.com/c/DrsulaimanAlhabibHospitals"))), + Utils.buildSvgWithAssets(icon: AppAssets.x2).onPress(() => launchUrl(Uri.parse("https://twitter.com/HMG"))), + Utils.buildSvgWithAssets(icon: AppAssets.fb).onPress(() => launchUrl(Uri.parse("https://www.facebook.com/DrSulaimanAlHabib?ref=tn_tnmn"))), + Utils.buildSvgWithAssets(icon: AppAssets.linkedin).onPress(() => launchUrl(Uri.parse("https://www.linkedin.com/company/drsulaiman-alhabib-medical-group"))), + Utils.buildSvgWithAssets(icon: AppAssets.youtube).onPress(() => launchUrl(Uri.parse("https://www.youtube.com/c/DrsulaimanAlhabibHospitals"))), ], ) ], @@ -546,9 +554,7 @@ class _FamilyCardWidgetState extends State { // final isParentUser = appState.getAuthenticatedUser()?.isParentUser ?? false; // final canSwitch = isParentUser || (!isParentUser && widget.profile.responseId == appState.getSuperUserID); return Container( - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, - borderRadius: 24.r, hasShadow: true, hasDenseShadow: true), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.r, hasShadow: true, hasDenseShadow: true), child: Column( children: [ Column( @@ -607,12 +613,14 @@ class _FamilyCardWidgetState extends State { labelText: "${LocaleKeys.fileno.tr(context: context)}: ${widget.profile.responseId}", iconSize: 12.w, ), - isActive ? AppCustomChipWidget( - icon: AppAssets.checkmark_icon, - labelText: LocaleKeys.verified.tr(context: context), - iconColor: AppColors.successColor, - labelPadding: EdgeInsetsDirectional.only(start: -6.w, end: 6.w), - ) : SizedBox.shrink(), + isActive + ? AppCustomChipWidget( + icon: AppAssets.checkmark_icon, + labelText: LocaleKeys.verified.tr(context: context), + iconColor: AppColors.successColor, + labelPadding: EdgeInsetsDirectional.only(start: -6.w, end: 6.w), + ) + : SizedBox.shrink(), ], ), ], @@ -639,73 +647,76 @@ class _FamilyCardWidgetState extends State { ), // isActive && appState.getAuthenticatedUser()!.bloodGroup != null // ? - isActive ? AppCustomChipWidget( - icon: AppAssets.blood_icon, + isActive + ? AppCustomChipWidget( + icon: AppAssets.blood_icon, labelPadding: EdgeInsetsDirectional.only(start: -6.w, end: 8.w), - labelText: appState.getAuthenticatedUser()!.bloodGroup ?? "N/A", - iconColor: AppColors.primaryRedColor) - : SizedBox(), + labelText: appState.getAuthenticatedUser()!.bloodGroup ?? "N/A", + iconColor: AppColors.primaryRedColor) + : SizedBox(), Consumer(builder: (context, insuranceVM, child) { return isActive ? AppCustomChipWidget( icon: insuranceVM.isInsuranceExpired ? AppAssets.cancel_circle_icon : insuranceVM.isInsuranceActive - ? AppAssets.insurance_active_icon - : AppAssets.alertSquare, - labelText: insuranceVM.isInsuranceExpired - ? LocaleKeys.insuranceExpired.tr(context: context) - : insuranceVM.isInsuranceActive - ? LocaleKeys.insuranceActive.tr(context: context) - : LocaleKeys.insuranceInActive.tr(context: context), - iconColor: insuranceVM.isInsuranceExpired - ? AppColors.primaryRedColor - : insuranceVM.isInsuranceActive - ? AppColors.successColor - : AppColors.warningColorYellow, - textColor: insuranceVM.isInsuranceExpired - ? AppColors.primaryRedColor - : insuranceVM.isInsuranceActive - ? AppColors.successColor - : AppColors.warningColorYellow, - iconSize: 12.w, - deleteIcon: insuranceVM.isInsuranceActive ? null : AppAssets.forward_chevron_icon, - deleteIconColor: insuranceVM.isInsuranceExpired - ? AppColors.primaryRedColor - : insuranceVM.isInsuranceActive - ? AppColors.successColor - : AppColors.warningColorYellow, - deleteIconHasColor: true, - onChipTap: () { - if (!insuranceVM.isInsuranceActive) { - insuranceVM.setIsInsuranceUpdateDetailsLoading(true); - insuranceVM.getPatientInsuranceDetailsForUpdate(appState.getAuthenticatedUser()!.patientId.toString(), appState.getAuthenticatedUser()!.patientIdentificationNo.toString()); - showCommonBottomSheetWithoutHeight(context, child: PatientInsuranceCardUpdateCard(), callBackFunc: () {}, title: "", isCloseButtonVisible: false, isFullScreen: false); - // showCommonBottomSheetWithoutHeight( - // title: LocaleKeys.notice.tr(context: context), - // context, - // child: Utils.getWarningWidget( - // loadingText: LocaleKeys.insuranceInActiveContactSupport.tr(context: context), - // confirmText: LocaleKeys.contactUs.tr(context: context), - // isShowActionButtons: true, - // onCancelTap: () { - // Navigator.pop(context); - // }, - // onConfirmTap: () async { - // launchUrl(Uri.parse("tel://" + "+966 92 006 6666")); - // }), - // callBackFunc: () {}, - // isFullScreen: false, - // isCloseButtonVisible: true, - // ); - } - }, - backgroundColor: insuranceVM.isInsuranceExpired - ? AppColors.primaryRedColor.withValues(alpha: 0.1) - : insuranceVM.isInsuranceActive - ? AppColors.successColor.withValues(alpha: 0.1) - : AppColors.warningColorYellow.withValues(alpha: 0.1), - labelPadding: EdgeInsetsDirectional.only(start: -4.w, end: insuranceVM.isInsuranceActive ? 6.w : 0.w), + ? AppAssets.insurance_active_icon + : AppAssets.alertSquare, + labelText: insuranceVM.isInsuranceExpired + ? LocaleKeys.insuranceExpired.tr(context: context) + : insuranceVM.isInsuranceActive + ? LocaleKeys.insuranceActive.tr(context: context) + : LocaleKeys.insuranceInActive.tr(context: context), + iconColor: insuranceVM.isInsuranceExpired + ? AppColors.primaryRedColor + : insuranceVM.isInsuranceActive + ? AppColors.successColor + : AppColors.warningColorYellow, + textColor: insuranceVM.isInsuranceExpired + ? AppColors.primaryRedColor + : insuranceVM.isInsuranceActive + ? AppColors.successColor + : AppColors.warningColorYellow, + iconSize: 12.w, + deleteIcon: insuranceVM.isInsuranceActive ? null : AppAssets.forward_chevron_icon, + deleteIconColor: insuranceVM.isInsuranceExpired + ? AppColors.primaryRedColor + : insuranceVM.isInsuranceActive + ? AppColors.successColor + : AppColors.warningColorYellow, + deleteIconHasColor: true, + onChipTap: () { + if (!insuranceVM.isInsuranceActive) { + insuranceVM.setIsInsuranceUpdateDetailsLoading(true); + insuranceVM.getPatientInsuranceDetailsForUpdate( + appState.getAuthenticatedUser()!.patientId.toString(), appState.getAuthenticatedUser()!.patientIdentificationNo.toString()); + showCommonBottomSheetWithoutHeight(context, + child: PatientInsuranceCardUpdateCard(), callBackFunc: () {}, title: "", isCloseButtonVisible: false, isFullScreen: false); + // showCommonBottomSheetWithoutHeight( + // title: LocaleKeys.notice.tr(context: context), + // context, + // child: Utils.getWarningWidget( + // loadingText: LocaleKeys.insuranceInActiveContactSupport.tr(context: context), + // confirmText: LocaleKeys.contactUs.tr(context: context), + // isShowActionButtons: true, + // onCancelTap: () { + // Navigator.pop(context); + // }, + // onConfirmTap: () async { + // launchUrl(Uri.parse("tel://" + "+966 92 006 6666")); + // }), + // callBackFunc: () {}, + // isFullScreen: false, + // isCloseButtonVisible: true, + // ); + } + }, + backgroundColor: insuranceVM.isInsuranceExpired + ? AppColors.primaryRedColor.withValues(alpha: 0.1) + : insuranceVM.isInsuranceActive + ? AppColors.successColor.withValues(alpha: 0.1) + : AppColors.warningColorYellow.withValues(alpha: 0.1), + labelPadding: EdgeInsetsDirectional.only(start: -4.w, end: insuranceVM.isInsuranceActive ? 6.w : 0.w), ).toShimmer2(isShow: insuranceVM.isInsuranceLoading) : SizedBox.shrink(); }), @@ -747,7 +758,7 @@ class _FamilyCardWidgetState extends State { height: isFoldable ? 50.h : 45.h, fontSize: 14.f, ).paddingOnly(top: 12.h, right: 16.w, left: 16.w, bottom: 16.h); - } + } Widget _buildNonParentUserButton(int? superUserId) { final canSwitchBack = superUserId != null && superUserId == widget.profile.responseId; diff --git a/lib/presentation/rate_appointment/rate_appointment_clinic.dart b/lib/presentation/rate_appointment/rate_appointment_clinic.dart index c8aedea8..e285a7c1 100644 --- a/lib/presentation/rate_appointment/rate_appointment_clinic.dart +++ b/lib/presentation/rate_appointment/rate_appointment_clinic.dart @@ -159,9 +159,9 @@ class _RateAppointmentClinicState extends State { Expanded( child: CustomButton( text: LocaleKeys.back.tr(context: context), - backgroundColor: Color(0xffFEE9EA), - borderColor: Color(0xffFEE9EA), - textColor: Color(0xffED1C2B), + backgroundColor: AppColors.secondaryLightRedColor, + borderColor: AppColors.secondaryLightRedColor, + textColor: AppColors.primaryRedColor, onPressed: () { appointmentRatingViewModel!.setTitle(LocaleKeys.rateDoctor.tr(context: context)); appointmentRatingViewModel!.setSubTitle(LocaleKeys.howWasYourLastVisitWithDoctor.tr(context: context)); diff --git a/lib/presentation/rate_appointment/rate_appointment_doctor.dart b/lib/presentation/rate_appointment/rate_appointment_doctor.dart index d78dc59e..b54b5741 100644 --- a/lib/presentation/rate_appointment/rate_appointment_doctor.dart +++ b/lib/presentation/rate_appointment/rate_appointment_doctor.dart @@ -181,9 +181,9 @@ class _RateAppointmentDoctorState extends State { Expanded( child: CustomButton( text: LocaleKeys.later.tr(context: context), - backgroundColor: Color(0xffFEE9EA), - borderColor: Color(0xffFEE9EA), - textColor: Color(0xffED1C2B), + backgroundColor: AppColors.secondaryLightRedColor, + borderColor: AppColors.secondaryLightRedColor, + textColor: AppColors.primaryRedColor, onPressed: () { Navigator.pop(context); }, diff --git a/lib/presentation/smartwatches/activity_detail.dart b/lib/presentation/smartwatches/activity_detail.dart index 0c17a9c0..888533af 100644 --- a/lib/presentation/smartwatches/activity_detail.dart +++ b/lib/presentation/smartwatches/activity_detail.dart @@ -67,8 +67,8 @@ class _ActivityDetailsState extends State { children: [ Expanded( child: CustomTabBar( - activeTextColor: Color(0xffED1C2B), - activeBackgroundColor: Color(0xffED1C2B).withValues(alpha: .1), + activeTextColor: AppColors.primaryRedColor, + activeBackgroundColor: AppColors.secondaryLightRedColor, tabs: [ CustomTabBarModel(null, "D"), CustomTabBarModel(null, "W"), diff --git a/lib/presentation/smartwatches/health_dashboard/health_dashboard.dart b/lib/presentation/smartwatches/health_dashboard/health_dashboard.dart index f30d87a9..84ec64e9 100644 --- a/lib/presentation/smartwatches/health_dashboard/health_dashboard.dart +++ b/lib/presentation/smartwatches/health_dashboard/health_dashboard.dart @@ -56,8 +56,8 @@ class _HealthDashboardState extends State with SingleTickerProv children: [ SizedBox(height: 96.h), CustomTabBar( - activeTextColor: Color(0xffED1C2B), - activeBackgroundColor: Color(0xffED1C2B).withValues(alpha: .1), + activeTextColor: AppColors.primaryRedColor, + activeBackgroundColor: AppColors.secondaryLightRedColor, tabs: [ CustomTabBarModel(null, "Overview".needTranslation), CustomTabBarModel(null, "Details".needTranslation), diff --git a/lib/presentation/symptoms_checker/organ_selector_screen.dart b/lib/presentation/symptoms_checker/organ_selector_screen.dart index 438d9728..5b5d8f2e 100644 --- a/lib/presentation/symptoms_checker/organ_selector_screen.dart +++ b/lib/presentation/symptoms_checker/organ_selector_screen.dart @@ -480,7 +480,7 @@ class _OrganSelectorPageState extends State with SingleTicker backgroundColor: AppColors.secondaryLightRedColor, textColor: AppColors.primaryRedColor, deleteIcon: AppAssets.cancel, - deleteIconColor: AppColors.primaryRedColor, + deleteIconColor: AppColors.criticalActionColor, deleteIconHasColor: false, onDeleteTap: () { viewModel.removeOrgan(organ.id); @@ -500,6 +500,7 @@ class _OrganSelectorPageState extends State with SingleTicker isDisabled: viewModel.selectedOrgans.isEmpty, backgroundColor: AppColors.primaryRedColor, textColor: AppColors.whiteColor, + borderColor: Colors.transparent, ), ); } diff --git a/lib/presentation/symptoms_checker/widgets/selected_organs_section.dart b/lib/presentation/symptoms_checker/widgets/selected_organs_section.dart index e2b342b6..a4f5f1ee 100644 --- a/lib/presentation/symptoms_checker/widgets/selected_organs_section.dart +++ b/lib/presentation/symptoms_checker/widgets/selected_organs_section.dart @@ -121,7 +121,7 @@ class _SelectedOrgansSectionState extends State { backgroundColor: AppColors.secondaryLightRedColor, textColor: AppColors.primaryRedColor, deleteIcon: AppAssets.cross_circle, - deleteIconColor: AppColors.primaryRedColor, + deleteIconColor: AppColors.criticalActionColor, deleteIconHasColor: true, onDeleteTap: () => widget.onRemoveOrgan(organ.id), padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.h), diff --git a/lib/presentation/symptoms_checker/widgets/triage_level_card.dart b/lib/presentation/symptoms_checker/widgets/triage_level_card.dart index 19ad3935..8633d830 100644 --- a/lib/presentation/symptoms_checker/widgets/triage_level_card.dart +++ b/lib/presentation/symptoms_checker/widgets/triage_level_card.dart @@ -94,7 +94,7 @@ class _TriageLevelCardState extends State with SingleTickerProv switch (triageLevel) { case 'emergency_ambulance': case 'emergency': - return AppColors.chipColorEmergency.withValues(alpha: 0.1); + return AppColors.primaryRedColor.withValues(alpha: 0.1); case 'consultation_24': case 'consultation': return AppColors.chipColorSeekMedicalAdvice.withValues(alpha: 0.1); diff --git a/lib/presentation/todo_section/widgets/ancillary_orders_list.dart b/lib/presentation/todo_section/widgets/ancillary_orders_list.dart index cc5bbbb6..da75dd7b 100644 --- a/lib/presentation/todo_section/widgets/ancillary_orders_list.dart +++ b/lib/presentation/todo_section/widgets/ancillary_orders_list.dart @@ -257,9 +257,9 @@ class AncillaryOrderCard extends StatelessWidget { onViewDetails!(); } }, - backgroundColor: Color(0xffFEE9EA), - borderColor: Color(0xffFEE9EA), - textColor: Color(0xffED1C2B), + backgroundColor: AppColors.secondaryLightRedColor, + borderColor: AppColors.secondaryLightRedColor, + textColor: AppColors.primaryRedColor, fontSize: (isFoldable || isTablet) ? 12.f : 14.f, fontWeight: FontWeight.w600, borderRadius: 10.r, diff --git a/lib/presentation/todo_section/widgets/ancillary_procedures_list.dart b/lib/presentation/todo_section/widgets/ancillary_procedures_list.dart index 54c92789..b7f0f28a 100644 --- a/lib/presentation/todo_section/widgets/ancillary_procedures_list.dart +++ b/lib/presentation/todo_section/widgets/ancillary_procedures_list.dart @@ -252,9 +252,9 @@ class AncillaryOrderCard extends StatelessWidget { onViewDetails!(); } }, - backgroundColor: Color(0xffFEE9EA), - borderColor: Color(0xffFEE9EA), - textColor: Color(0xffED1C2B), + backgroundColor: AppColors.secondaryLightRedColor, + borderColor: AppColors.secondaryLightRedColor, + textColor: AppColors.primaryRedColor, fontSize: 14.f, isBold: true, borderRadius: 10.r, diff --git a/lib/presentation/water_monitor/widgets/cup_bottomsheet_widgets.dart b/lib/presentation/water_monitor/widgets/cup_bottomsheet_widgets.dart index 058f14e8..aef892f9 100644 --- a/lib/presentation/water_monitor/widgets/cup_bottomsheet_widgets.dart +++ b/lib/presentation/water_monitor/widgets/cup_bottomsheet_widgets.dart @@ -91,7 +91,7 @@ class SwitchCupBottomSheet extends StatelessWidget { width: 60.w, height: 60.w, decoration: BoxDecoration( - color: isSelected ? AppColors.primaryRedColor.withOpacity(0.08) : Colors.transparent, + color: isSelected ? AppColors.secondaryLightRedColor : Colors.transparent, borderRadius: BorderRadius.circular(12.r), border: Border.all( color: isSelected ? AppColors.primaryRedColor : AppColors.bgScaffoldColor, @@ -271,9 +271,9 @@ class _CustomizeCupBottomSheetState extends State { SliderTheme( data: SliderTheme.of(context).copyWith( activeTrackColor: AppColors.primaryRedColor, - inactiveTrackColor: AppColors.primaryRedColor.withOpacity(0.2), + inactiveTrackColor: AppColors.secondaryLightRedColor, thumbColor: AppColors.primaryRedColor, - overlayColor: AppColors.primaryRedColor.withOpacity(0.2), + overlayColor: AppColors.secondaryLightRedColor, trackHeight: 4.h, thumbShape: RoundSliderThumbShape(enabledThumbRadius: 10.w), ), diff --git a/lib/presentation/weather/weather_details_page.dart b/lib/presentation/weather/weather_details_page.dart index 8762a55a..50dbefa3 100644 --- a/lib/presentation/weather/weather_details_page.dart +++ b/lib/presentation/weather/weather_details_page.dart @@ -62,7 +62,7 @@ class _WeatherDetailsPageState extends State { case ColorName.ORANGE: return AppColors.warningLightColor.withValues(alpha: 0.1); case ColorName.RED: - return AppColors.errorLightColor.withValues(alpha: 0.1); + return AppColors.primaryRedColor.withValues(alpha: 0.1); } } @@ -120,7 +120,7 @@ class _WeatherDetailsPageState extends State { } // Red: Very High, At High Risk else if (category.contains('very high') || category.contains('at high risk')) { - return AppColors.errorLightColor.withValues(alpha: 0.1); + return AppColors.primaryRedColor.withValues(alpha: 0.1); } // Orange: Risk, High (but not "very high" or "at high risk") else if (category.contains('risk') || category.contains('high')) { diff --git a/lib/services/remote_config_service.dart b/lib/services/remote_config_service.dart new file mode 100644 index 00000000..568bd4e6 --- /dev/null +++ b/lib/services/remote_config_service.dart @@ -0,0 +1,83 @@ +import 'dart:convert'; + +import 'package:firebase_remote_config/firebase_remote_config.dart'; +import 'package:flutter/foundation.dart' show kDebugMode; +import 'package:flutter/services.dart' show rootBundle; +import 'package:hmg_patient_app_new/services/logger_service.dart'; + +/// Firebase Remote Config wrapper. Fully generic — the app never hardcodes +/// occasion-specific values here; it only knows about the `occasion_config` +/// remote key and reads whatever JSON is published on the Firebase console. +abstract class RemoteConfigService { + /// Must be called once at startup, before the first frame if possible. + Future initialize(); + + /// Fetches the latest values from Firebase (respecting the configured + /// minimum fetch interval) and activates them. + Future fetchAndActivate(); + + /// Raw JSON string currently held for the `occasion_config` key + /// (bundled default until the first successful fetch/activate). + String get occasionConfigRawJson; +} + +class RemoteConfigServiceImp implements RemoteConfigService { + static const String occasionConfigKey = 'occasion_config'; + static const String _defaultConfigAssetPath = 'assets/json/default_occasion_config.json'; + + final LoggerService loggerService; + final FirebaseRemoteConfig _remoteConfig; + + RemoteConfigServiceImp({ + required this.loggerService, + FirebaseRemoteConfig? remoteConfig, + }) : _remoteConfig = remoteConfig ?? FirebaseRemoteConfig.instance; + + @override + Future initialize() async { + try { + await _remoteConfig.setConfigSettings( + RemoteConfigSettings( + fetchTimeout: const Duration(seconds: 10), + // In debug builds, always fetch fresh values so you can test + // Remote Config changes instantly without waiting/throttling. + // Release builds keep a sane 1-hour minimum interval. + minimumFetchInterval: kDebugMode ? Duration.zero : const Duration(hours: 1), + ), + ); + + // Bundled fallback so behaviour is deterministic (and identical to the + // current app look) before the very first successful remote fetch. + final defaultJson = await rootBundle.loadString(_defaultConfigAssetPath); + await _remoteConfig.setDefaults({occasionConfigKey: defaultJson}); + } catch (ex) { + loggerService.logError('RemoteConfigService.initialize error: $ex'); + } + } + + @override + Future fetchAndActivate() async { + try { + return await _remoteConfig.fetchAndActivate(); + } catch (ex) { + loggerService.logError('RemoteConfigService.fetchAndActivate error: $ex'); + return false; + } + } + + @override + String get occasionConfigRawJson { + try { + final value = _remoteConfig.getString(occasionConfigKey); + if (value.isEmpty) return _fallbackDisabledJson; + // Validate JSON shape early; if malformed, fall back safely. + json.decode(value); + return value; + } catch (ex) { + loggerService.logError('RemoteConfigService.occasionConfigRawJson parse error: $ex'); + return _fallbackDisabledJson; + } + } + + static const String _fallbackDisabledJson = '{"key":"none","enabled":false}'; +} diff --git a/lib/theme/colors.dart b/lib/theme/colors.dart index 1ecbcb61..3b9cbdae 100644 --- a/lib/theme/colors.dart +++ b/lib/theme/colors.dart @@ -5,41 +5,115 @@ class AppColors { /// When true, every getter below returns the dark palette value. static bool isDarkMode = false; + /// Global occasion-theme flag — toggled by OccasionThemeViewModel based on + /// Firebase Remote Config. When false (the default), the app looks exactly + /// like it does today — no colors are affected. + static bool isOccasionThemeActive = false; + + /// Remotely-configured primary color for the currently active occasion + /// (e.g. National Day). Null unless an occasion theme is active. + static Color? occasionPrimaryColor; + + /// Lighter/translucent (11% opacity) variant of [occasionPrimaryColor], + /// derived automatically by OccasionThemeViewModel. Null unless an + /// occasion theme is active. + static Color? occasionSecondaryColor; + static const transparent = Colors.transparent; // ── Scaffold / Background ───────────────────────────────────────────────── 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); + static Color get greyInfoTextColor => isDarkMode ? dark.greyInfoTextColor : const Color(0xff777777); + static Color get lightGrayColor => isDarkMode ? dark.lightGrayColor : const Color(0xff808080); + static Color get greyTextColorLight => isDarkMode ? dark.greyTextColorLight : const Color(0xFFA2A2A2); + static Color get mainPurple => isDarkMode ? dark.mainPurple : const Color(0xFF7954F7); // ── Core Colors ─────────────────────────────────────────────────────────── static Color get whiteColor => isDarkMode ? dark.whiteColor : const Color(0xFFffffff); + // static const Color primaryRedColor = Color(0xFFED1C2B); - static Color get primaryRedColor => isDarkMode ? dark.primaryRedColor : Color(0xFFED1C2B); - static Color get secondaryLightRedColor => isDarkMode ? dark.secondaryLightRedColor : const Color(0xFFFEE9EA); + /// Occasion-aware primary brand color. Resolves to the remotely-configured + /// occasion color when active (e.g. National Day green), otherwise falls + /// back to the original dark/light red — identical to current behavior. + /// + /// NOTE: Do NOT use this getter for `.withOpacity()/.withValues()` derived + /// (translucent) colors — use [primaryRedColorBase] for those instead, so + /// occasion theming never affects translucent overlays/backgrounds. + static Color get primaryRedColor => (isOccasionThemeActive && occasionPrimaryColor != null) ? occasionPrimaryColor! : primaryRedColorBase; + + /// The original, non-themed brand red (or its dark-mode variant). Always + /// stable regardless of any active occasion theme — intended for opacity + /// derived usages (e.g. `AppColors.primaryRedColorBase.withOpacity(0.1)`). + static Color get primaryRedColorBase => isDarkMode ? dark.primaryRedColor : const Color(0xFFED1C2B); + + /// Semantic color for critical/negative actions (delete/deactivate account, + /// cancel appointment, clear/reset, emergency, etc.). Same light/dark + /// values as [primaryRedColorBase] — respects dark mode, but is never + /// affected by the active occasion/remote-config theme, so destructive + /// actions always read as "red" regardless of any occasion branding. + static Color get criticalActionColor => primaryRedColorBase; + + /// Occasion-aware light/tint variant of [primaryRedColor] (e.g. chip and + /// badge backgrounds). Resolves to the remotely-configured occasion's + /// secondary color when active (e.g. National Day's light green), + /// otherwise falls back to the original dark/light tint — identical to + /// current behavior. + /// + /// NOTE: Do NOT use this getter for `.withOpacity()/.withValues()` derived + /// colors — use [secondaryLightRedColorBase] for those instead, so + /// occasion theming never affects translucent overlays/backgrounds. + static Color get secondaryLightRedColor => (isOccasionThemeActive && occasionSecondaryColor != null) ? occasionSecondaryColor! : secondaryLightRedColorBase; + + /// The original, non-themed light-red tint (or its dark-mode variant). + /// Always stable regardless of any active occasion theme — intended for + /// opacity derived usages. + static Color get secondaryLightRedColorBase => isDarkMode ? dark.secondaryLightRedColor : const Color(0xFFFEE9EA); static const Color successColor = Color(0xFF18C273); + static Color get successLightBgColor => isDarkMode ? dark.successLightBgColor : const Color(0xffDDF6EA); + static Color get greyColor => isDarkMode ? dark.greyColor : const Color(0xFFEFEFF0); + static Color get chipBgColor => isDarkMode ? dark.chipBgColor : const Color(0xFFEFEFF0); + static Color get textColor => isDarkMode ? dark.textColor : const Color(0xFF2E3039); + static Color get iconColor => isDarkMode ? dark.iconColor : const Color(0xFF2E3039); + static Color get borderGrayColor => isDarkMode ? dark.borderGrayColor : const Color(0x332E3039); + static Color get textColorLight => isDarkMode ? dark.textColorLight : const Color(0xFF5E5E5E); + static Color get dividerColor => isDarkMode ? dark.dividerColor : const Color(0x40D2D2D2); + static Color get warningColorYellow => isDarkMode ? dark.warningColorYellow : const Color(0xFFF4A308); + static Color get inputLabelTextColor => isDarkMode ? dark.inputLabelTextColor : const Color(0xff898A8D); + static Color get greyTextColor => isDarkMode ? dark.greyTextColor : const Color(0xFF8F9AA3); + static Color get lightGrayBGColor => isDarkMode ? dark.lightGrayBGColor : const Color(0x142E3039); + static Color get checkBoxBorderColor => isDarkMode ? dark.checkBoxBorderColor : const Color(0xffD2D2D2); + static Color get pharmacyBGColor => isDarkMode ? dark.pharmacyBGColor : const Color(0xFF359846); + static Color get lightGreenColor => isDarkMode ? dark.lightGreenColor : const Color(0xFF0ccedde); + static Color get ratingColorYellow => isDarkMode ? dark.ratingColorYellow : const Color(0xFFFFAF15); + static Color get spacerLineColor => isDarkMode ? dark.spacerLineColor : const Color(0x2E30391A); + static Color get habibPharmacyColor => const Color(0xFF2d9137); // ── Doctor Rating Colors ────────────────────────────────────────────────── @@ -52,92 +126,147 @@ class AppColors { 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); + static Color get infoColor => isDarkMode ? dark.infoColor : const Color(0xFF0B85F7); + static Color get warningColor => isDarkMode ? dark.warningColor : const Color(0xFFFFCC00); + static Color get switchBackgroundColor => isDarkMode ? dark.switchBackgroundColor : const Color(0x2618C273); + static Color get bottomNAVBorder => isDarkMode ? dark.bottomNAVBorder : const Color(0xFFEEEEEE); + static Color get quickLoginColor => isDarkMode ? dark.quickLoginColor : const Color(0xFF666666); + static Color get tooltipTextColor => isDarkMode ? dark.tooltipTextColor : const Color(0xFF414D55); + static Color get graphGridColor => isDarkMode ? dark.graphGridColor : const Color(0x4D18C273); + static Color get highAndLow => isDarkMode ? dark.highAndLow : const Color(0xFFFFAF15); + static Color get labelTextColor => isDarkMode ? dark.labelTextColor : const Color(0xFF838383); + static Color get calenderTextColor => isDarkMode ? dark.calenderTextColor : const Color(0xFFD0D0D0); + static Color get lightGreenButtonColor => isDarkMode ? dark.lightGreenButtonColor : const Color(0x2618C273); + static Color get lightRedButtonColor => isDarkMode ? dark.lightRedButtonColor : const Color(0x1AED1C2B); // ── Status Colors ───────────────────────────────────────────────────────── static Color get statusPendingColor => isDarkMode ? dark.statusPendingColor : const Color(0xffCC9B14); + static Color get statusProcessingColor => isDarkMode ? dark.statusProcessingColor : const Color(0xff2E303A); + static Color get statusCompletedColor => isDarkMode ? dark.statusCompletedColor : const Color(0xff359846); + static Color get statusRejectedColor => isDarkMode ? dark.statusRejectedColor : const Color(0xffD02127); // ── Info Banner Colors ──────────────────────────────────────────────────── static Color get infoBannerBgColor => isDarkMode ? dark.infoBannerBgColor : const Color(0xFFFFF4E6); + static Color get infoBannerBorderColor => isDarkMode ? dark.infoBannerBorderColor : const Color(0xFFFFE5B4); + static Color get infoBannerIconColor => isDarkMode ? dark.infoBannerIconColor : const Color(0xFFCC9B14); + static Color get infoBannerTextColor => isDarkMode ? dark.infoBannerTextColor : const Color(0xFF856404); // ── Symptoms Checker ─────────────────────────────────────────────��──────── static Color get chipColorSeekMedicalAdvice => isDarkMode ? dark.chipColorSeekMedicalAdvice : const Color(0xFFFFAF15); + static Color get chipTextColorSeekMedicalAdvice => isDarkMode ? dark.chipTextColorSeekMedicalAdvice : const Color(0xFFD48D05); // ── Services Page Colors ────────────────────────────────────────────────── static Color get eReferralCardColor => isDarkMode ? dark.eReferralCardColor : const Color(0xFFFF8012); + static Color get bloodDonationCardColor => isDarkMode ? dark.bloodDonationCardColor : const Color(0xFFFF5662); + static Color get myChildVaccineCardColor => isDarkMode ? dark.myChildVaccineCardColor : const Color(0xFFFF2D78); + static Color get bookAppointment => isDarkMode ? dark.bookAppointment : const Color(0xFF415364); // ── Health Calculators Colors ───────────────────────────────────────────── static Color get bmiCalculatorColor => isDarkMode ? dark.bmiCalculatorColor : const Color(0xFFF43333); + static Color get caloriesCalculatorColor => isDarkMode ? dark.caloriesCalculatorColor : const Color(0xFF0B85F7); + static Color get bmrCalculatorColor => isDarkMode ? dark.bmrCalculatorColor : const Color(0xFF18C273); + static Color get idealBodyWeightColor => isDarkMode ? dark.idealBodyWeightColor : const Color(0xFFFF8012); + static Color get bodyFatCalculatorColor => isDarkMode ? dark.bodyFatCalculatorColor : const Color(0xFFFF5662); + static Color get carbsProteinFatColor => isDarkMode ? dark.carbsProteinFatColor : const Color(0xFF18C273); + static Color get ovulationPeriodColor => isDarkMode ? dark.ovulationPeriodColor : const Color(0xFFFF47A9); + static Color get deliveryDueDateColor => isDarkMode ? dark.deliveryDueDateColor : const Color(0xFFFF2D78); // ── Water Monitor ───────────────────────────────────────────────────────── static Color get blueColor => isDarkMode ? dark.blueColor : const Color(0xFF4EB5FF); + static Color get blueGradientColorOne => isDarkMode ? dark.blueGradientColorOne : const Color(0xFFF1F7FD); + static Color get blueGradientColorTwo => isDarkMode ? dark.blueGradientColorTwo : const Color(0xFFD9EFFF); // ── Shimmer ─────────────────────────────────────────────────────────────── static Color get shimmerBaseColor => isDarkMode ? dark.shimmerBaseColor : const Color(0xFFE0E0E0); + static Color get shimmerHighlightColor => isDarkMode ? dark.shimmerHighlightColor : const Color(0xFFF5F5F5); + static Color get covid29Color => isDarkMode ? dark.covid29Color : const Color(0xff2563EB); + static Color get lightGreyTextColor => isDarkMode ? dark.lightGreyTextColor : const Color(0xFF959595); + static Color get labelColorYellow => isDarkMode ? dark.labelColorYellow : const Color(0xFFFBCB6E); - static const LinearGradient aiLinearGradient = - LinearGradient(colors: [Color(0xFF8A38F5), Color(0xFFE20BBB)], begin: Alignment.topLeft, end: Alignment.bottomRight); + static const LinearGradient aiLinearGradient = LinearGradient(colors: [Color(0xFF8A38F5), Color(0xFFE20BBB)], begin: Alignment.topLeft, end: Alignment.bottomRight); // ── Aliases ─────────────────────────────────────────────────────────────── static Color get bgScaffoldColor => scaffoldBgColor; + static Color get primaryRedBorderColor => primaryRedColor; + static Color get chipPrimaryRedBorderColor => primaryRedColor; + static Color get criticalLowAndHigh => primaryRedColor; + static Color get chipColorEmergency => primaryRedColor; + static Color get errorLightColor => primaryRedColor; + static Color get secondaryLightRedBorderColor => secondaryLightRedColor; + static Color get bgRedLightColor => secondaryLightRedColor; + static Color get chipSecondaryLightRedColor => secondaryLightRedColor; + static Color get blackBgColor => textColor; + static Color get blackColor => textColor; + static Color get borderOnlyColor => textColor; + static Color get chipBorderColorOpacity20 => borderGrayColor; + static Color get bgGreenColor => successColor; + static Color get textGreenColor => successColor; + static Color get successLightColor => successColor; + static Color get thumbColor => successColor; + static Color get chipColorMonitor => successColor; + static Color get greyLightColor => greyColor; + static Color get alertLightColor => alertColor; + static Color get infoLightColor => infoColor; + static Color get warningLightColor => warningColor; // ── Dark Mode Palette ───────────────────────────────────────────────────── @@ -152,11 +281,17 @@ class AppColorsDark { // Scaffold / Background Color get scaffoldBgColor => const Color(0xFF191919); + Color get bottomSheetBgColor => const Color(0xFF1C1C1E); + Color get lightGreyEFColor => const Color(0xFF2A2A3A); + Color get greyF7Color => const Color(0xFF1E1E1E); + Color get greyInfoTextColor => const Color(0xFF9E9E9E); + Color get lightGrayColor => const Color(0xFF9E9E9E); + Color get greyTextColorLight => const Color(0xFF757575); // ⚠️ NEW — was missing from AppColorsDark @@ -164,114 +299,192 @@ class AppColorsDark { // Core Color get baseColor => const Color(0xFF1E1E1E); + // ⚠️ NEW — maps whiteColor → dark surface color Color get whiteColor => const Color(0xFF1E1E1E); // surface replacement for white Color get primaryRedColor => const Color(0xFFDE5C5D); + Color get secondaryLightRedColor => const Color(0xFF3A1015); + Color get successColor => const Color(0xFF18C273); + Color get successLightBgColor => const Color(0xFF0D2E1F); + Color get textColor => const Color(0xFFFFFFFF); + Color get iconColor => const Color(0xFFFFFFFF); + Color get borderGrayColor => const Color(0x55ECECEC); + Color get textColorLight => const Color(0xFFB0B0B0); + Color get dividerColor => const Color(0x33FFFFFF); + Color get warningColorYellow => const Color(0xFFF4A308); + Color get inputLabelTextColor => const Color(0xFF9E9E9E); + Color get greyTextColor => const Color(0xFF8F9AA3); + Color get lightGrayBGColor => const Color(0x22FFFFFF); + Color get checkBoxBorderColor => const Color(0xFF555555); + Color get pharmacyBGColor => const Color(0xFF359846); + // ⚠️ NEW — was missing from AppColorsDark Color get lightGreenColor => const Color(0xFF0ccedde); // same teal accent — works on dark Color get ratingColorYellow => const Color(0xFFFFAF15); + Color get spacerLineColor => const Color(0x22FFFFFF); // Semantic / Status Color get errorColor => const Color(0xFFD63D48); + Color get alertColor => const Color(0xFFD48D05); + Color get infoColor => const Color(0xFF4DA6FF); + Color get warningColor => const Color(0xFFFFCC00); + Color get greyColor => const Color(0xFF2C2C2E); + Color get chipBgColor => const Color(0xFF3E3E3E); + Color get cardDarkBorderColor => const Color(0x0DEAEAEA); + Color get serviceCardButtonColor => const Color(0xFF3E3E3E); + Color get switchBackgroundColor => const Color(0x2618C273); + Color get bottomNAVBorder => const Color(0xFF2C2C2E); + Color get quickLoginColor => const Color(0xFF9E9E9E); + Color get tooltipTextColor => const Color(0xFFCFD8DC); + Color get graphGridColor => const Color(0x4D18C273); + Color get highAndLow => const Color(0xFFFFAF15); + Color get labelTextColor => const Color(0xFF9E9E9E); + Color get calenderTextColor => const Color(0xFF555555); + Color get lightGreenButtonColor => const Color(0x2618C273); + Color get lightRedButtonColor => const Color(0x1AED1C2B); // Status Color get statusPendingColor => const Color(0xFFCC9B14); + Color get statusProcessingColor => const Color(0xFFECECEC); + Color get statusCompletedColor => const Color(0xFF359846); + Color get statusRejectedColor => const Color(0xFFD02127); // Info Banner Color get infoBannerBgColor => const Color(0xFF2A1F0A); + Color get infoBannerBorderColor => const Color(0xFF5A3D0A); + Color get infoBannerIconColor => const Color(0xFFCC9B14); + Color get infoBannerTextColor => const Color(0xFFFFCB6B); // Symptoms Checker Color get chipColorSeekMedicalAdvice => const Color(0xFFFFAF15); + Color get chipTextColorSeekMedicalAdvice => const Color(0xFFD48D05); // Services Page Color get eReferralCardColor => const Color(0xFFFF8012); + Color get bloodDonationCardColor => const Color(0xFFFF5662); + Color get myChildVaccineCardColor => const Color(0xFFFF2D78); + Color get bookAppointment => const Color(0xFF415364); // Health Calculators Color get bmiCalculatorColor => const Color(0xFFF43333); + Color get caloriesCalculatorColor => const Color(0xFF0B85F7); + Color get bmrCalculatorColor => const Color(0xFF18C273); + Color get idealBodyWeightColor => const Color(0xFFFF8012); + Color get bodyFatCalculatorColor => const Color(0xFFFF5662); + Color get carbsProteinFatColor => const Color(0xFF18C273); + Color get ovulationPeriodColor => const Color(0xFFFF47A9); + Color get deliveryDueDateColor => const Color(0xFFFF2D78); // Water Monitor Color get blueColor => const Color(0xFF4EB5FF); + Color get blueGradientColorOne => const Color(0xFF101C2A); + Color get blueGradientColorTwo => const Color(0xFF0D2235); // Shimmer Color get shimmerBaseColor => const Color(0xFF2C2C2C); + Color get shimmerHighlightColor => const Color(0xFF3D3D3D); + Color get covid29Color => const Color(0xFF2563EB); + Color get lightGreyTextColor => const Color(0xFF757575); + Color get labelColorYellow => const Color(0xFFFBCB6E); // Aliases Color get bgScaffoldColor => scaffoldBgColor; + Color get primaryRedBorderColor => primaryRedColor; + Color get chipPrimaryRedBorderColor => primaryRedColor; + Color get criticalLowAndHigh => primaryRedColor; + Color get chipColorEmergency => primaryRedColor; + Color get errorLightColor => primaryRedColor; + Color get secondaryLightRedBorderColor => secondaryLightRedColor; + Color get bgRedLightColor => secondaryLightRedColor; + Color get chipSecondaryLightRedColor => secondaryLightRedColor; + Color get blackBgColor => textColor; + Color get blackColor => textColor; + Color get borderOnlyColor => textColor; + Color get chipBorderColorOpacity20 => borderGrayColor; + Color get bgGreenColor => successColor; + Color get textGreenColor => successColor; + Color get successLightColor => successColor; + Color get thumbColor => successColor; + Color get chipColorMonitor => successColor; + Color get greyLightColor => greyColor; + Color get alertLightColor => alertColor; + Color get infoLightColor => infoColor; + Color get warningLightColor => warningColor; } @@ -283,61 +496,99 @@ extension AppColorsContext on BuildContext { // Scaffold / Background Color get scaffoldBgColor => _isDark ? AppColors.dark.scaffoldBgColor : const Color(0xFFF8F8F8); + Color get bottomSheetBgColor => _isDark ? AppColors.dark.bottomSheetBgColor : const Color(0xFFF8F8FA); + Color get greyF7Color => _isDark ? AppColors.dark.greyF7Color : const Color(0xffF7F7F7); + Color get greyInfoTextColor => _isDark ? AppColors.dark.greyInfoTextColor : const Color(0xff777777); + Color get lightGrayColor => _isDark ? AppColors.dark.lightGrayColor : const Color(0xff808080); // Core Color get cardBaseColor => _isDark ? AppColors.dark.baseColor : const Color(0xFFFFFFFF); + Color get primaryRedColor => _isDark ? AppColors.dark.primaryRedColor : AppColors.primaryRedColor; + Color get secondaryLightRedColor => _isDark ? AppColors.dark.serviceCardButtonColor : const Color(0xFFFEE9EA); + Color get secondaryLightRedForegroundColor => _isDark ? const Color(0xFFFFFFFF) : AppColors.primaryRedColor; + Color get successColor => AppColors.successColor; + Color get successLightBgColor => _isDark ? AppColors.dark.successLightBgColor : const Color(0xffDDF6EA); + Color get textColor => _isDark ? AppColors.dark.textColor : const Color(0xFF2E3039); + Color get iconColor => _isDark ? AppColors.dark.iconColor : const Color(0xFF2E3039); + Color get borderGrayColor => _isDark ? AppColors.dark.borderGrayColor : const Color(0x332E3039); + Color get textColorLight => _isDark ? AppColors.dark.textColorLight : const Color(0xFF5E5E5E); + Color get dividerColor => _isDark ? AppColors.dark.dividerColor : const Color(0x40D2D2D2); + Color get inputLabelTextColor => _isDark ? AppColors.dark.inputLabelTextColor : const Color(0xff898A8D); + Color get greyTextColor => AppColors.greyTextColor; + Color get lightGrayBGColor => _isDark ? AppColors.dark.lightGrayBGColor : const Color(0x142E3039); + Color get checkBoxBorderColor => _isDark ? AppColors.dark.checkBoxBorderColor : const Color(0xffD2D2D2); // Status / Semantic Color get errorColor => _isDark ? AppColors.dark.errorColor : const Color(0xFFED1C2B); + Color get alertColor => AppColors.alertColor; + Color get infoColor => _isDark ? AppColors.dark.infoColor : const Color(0xFF0B85F7); + Color get warningColor => AppColors.warningColor; + Color get greyColor => _isDark ? AppColors.dark.greyColor : const Color(0xFFEFEFF0); + Color get chipBgColor => _isDark ? AppColors.dark.chipBgColor : const Color(0xFFEFEFF0); + Color get cardDarkBorderColor => _isDark ? AppColors.dark.cardDarkBorderColor : Colors.transparent; + Color get serviceCardButtonColor => _isDark ? AppColors.dark.serviceCardButtonColor : const Color(0xFF2E3039); + Color get bottomNAVBorder => _isDark ? AppColors.dark.bottomNAVBorder : const Color(0xFFEEEEEE); + Color get labelTextColor => _isDark ? AppColors.dark.labelTextColor : const Color(0xFF838383); + Color get lightGreyTextColor => _isDark ? AppColors.dark.lightGreyTextColor : const Color(0xFF959595); // Info Banner Color get infoBannerBgColor => _isDark ? AppColors.dark.infoBannerBgColor : const Color(0xFFFFF4E6); + Color get infoBannerBorderColor => _isDark ? AppColors.dark.infoBannerBorderColor : const Color(0xFFFFE5B4); + Color get infoBannerIconColor => AppColors.infoBannerIconColor; + Color get infoBannerTextColor => _isDark ? AppColors.dark.infoBannerTextColor : const Color(0xFF856404); // Water Monitor Color get blueColor => AppColors.blueColor; + Color get blueGradientColorOne => _isDark ? AppColors.dark.blueGradientColorOne : const Color(0xFFF1F7FD); + Color get blueGradientColorTwo => _isDark ? AppColors.dark.blueGradientColorTwo : const Color(0xFFD9EFFF); // 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; + Color get bgGreenColor => successColor; + Color get bgRedLightColor => secondaryLightRedColor; + Color get blackColor => textColor; + Color get blackBgColor => textColor; } diff --git a/lib/widgets/appbar/collapsing_list_view.dart b/lib/widgets/appbar/collapsing_list_view.dart index c3ebe4ba..0c677002 100644 --- a/lib/widgets/appbar/collapsing_list_view.dart +++ b/lib/widgets/appbar/collapsing_list_view.dart @@ -115,8 +115,7 @@ class CollapsingListView extends StatelessWidget { margin: EdgeInsets.fromLTRB(24.w, 0, 24.w, 0), child: Transform.flip( flipX: appState.isArabic(), - child: Utils.buildSvgWithAssets( - icon: isClose ? AppAssets.close_bottom_nav_trans : AppAssets.arrow_back_new, width: 24.h, height: 24.h), + child: Utils.buildSvgWithAssets(icon: isClose ? AppAssets.close_bottom_nav_trans : AppAssets.arrow_back_new, width: 24.h, height: 24.h), ), ).onPress(() { if (leadingCallback != null) { @@ -289,8 +288,7 @@ class _ScrollAnimatedTitleState extends State { return Container( // height: (widget.preferredSize.height - _fontSize / 2).h, height: 56.h, - alignment: - isRtl ? (widget.showBack ? Alignment.topRight : Alignment.centerRight) : (widget.showBack ? Alignment.topLeft : Alignment.centerLeft), + alignment: isRtl ? (widget.showBack ? Alignment.topRight : Alignment.centerRight) : (widget.showBack ? Alignment.topLeft : Alignment.centerLeft), padding: EdgeInsets.fromLTRB(24.w, 0, 24.w, 0), child: Row( spacing: 4.h, @@ -306,38 +304,19 @@ class _ScrollAnimatedTitleState extends State { ), ).expanded, ...[ - if (widget.logout != null) - actionButton(context, t, title: LocaleKeys.logout.tr(context: context), icon: AppAssets.logout).onPress(widget.logout!), - if (widget.report != null) - actionButton(context, t, title: LocaleKeys.feedback.tr(context: context), icon: AppAssets.report_icon).onPress(widget.report!), - if (widget.history != null) - actionButton(context, t, title: LocaleKeys.history.tr(context: context), icon: AppAssets.insurance_history_icon) - .onPress(widget.history!), - if (widget.instructions != null) - actionButton(context, t, title: LocaleKeys.instructions.tr(context: context), icon: AppAssets.requests).onPress(widget.instructions!), - if (widget.requests != null) - actionButton(context, t, title: LocaleKeys.requests.tr(context: context), icon: AppAssets.insurance_history_icon) - .onPress(widget.requests!), - if (widget.sendEmail != null) - actionButton(context, t, title: LocaleKeys.sendEmail.tr(context: context), icon: AppAssets.email).onPress(widget.sendEmail!), - if (widget.doctorResponse != null) - actionButton(context, t, title: LocaleKeys.doctorResponses.tr(context: context), icon: AppAssets.doctorResponseIcon) - .onPress(widget.doctorResponse!), + if (widget.logout != null) actionButton(context, t, title: LocaleKeys.logout.tr(context: context), icon: AppAssets.logout).onPress(widget.logout!), + if (widget.report != null) actionButton(context, t, title: LocaleKeys.feedback.tr(context: context), icon: AppAssets.report_icon).onPress(widget.report!), + if (widget.history != null) actionButton(context, t, title: LocaleKeys.history.tr(context: context), icon: AppAssets.insurance_history_icon).onPress(widget.history!), + if (widget.instructions != null) actionButton(context, t, title: LocaleKeys.instructions.tr(context: context), icon: AppAssets.requests).onPress(widget.instructions!), + if (widget.requests != null) actionButton(context, t, title: LocaleKeys.requests.tr(context: context), icon: AppAssets.insurance_history_icon).onPress(widget.requests!), + if (widget.sendEmail != null) actionButton(context, t, title: LocaleKeys.sendEmail.tr(context: context), icon: AppAssets.email).onPress(widget.sendEmail!), + if (widget.doctorResponse != null) actionButton(context, t, title: LocaleKeys.doctorResponses.tr(context: context), icon: AppAssets.doctorResponseIcon).onPress(widget.doctorResponse!), if (widget.search != null) Utils.buildSvgWithAssets(icon: AppAssets.search_icon).onPress(widget.search!), - if (widget.aiOverview != null) - actionButton(context, t, title: LocaleKeys.aiOverView.tr(context: context), icon: AppAssets.aiOverView, isAiButton: true) - .onPress(widget.aiOverview!), - if (widget.downloadReport != null) - actionButton(context, t, title: LocaleKeys.downloadReport.tr(context: context), icon: AppAssets.download) - .onPress(widget.downloadReport!), - if (widget.viewImage != null) - actionButton(context, t, title: LocaleKeys.viewRadiologyImage.tr(context: context), icon: AppAssets.download) - .onPress(widget.viewImage!), - if (widget.location != null) - actionButton(context, t, title: LocaleKeys.sortByLocation.tr(context: context), icon: AppAssets.location).onPress(widget.location!), - if (widget.downloadInvoice != null) - actionButton(context, t, title: LocaleKeys.downloadInvoice.tr(context: context), icon: AppAssets.download) - .onPress(widget.downloadInvoice!), + if (widget.aiOverview != null) actionButton(context, t, title: LocaleKeys.aiOverView.tr(context: context), icon: AppAssets.aiOverView, isAiButton: true).onPress(widget.aiOverview!), + if (widget.downloadReport != null) actionButton(context, t, title: LocaleKeys.downloadReport.tr(context: context), icon: AppAssets.download).onPress(widget.downloadReport!), + if (widget.viewImage != null) actionButton(context, t, title: LocaleKeys.viewRadiologyImage.tr(context: context), icon: AppAssets.download).onPress(widget.viewImage!), + if (widget.location != null) actionButton(context, t, title: LocaleKeys.sortByLocation.tr(context: context), icon: AppAssets.location).onPress(widget.location!), + if (widget.downloadInvoice != null) actionButton(context, t, title: LocaleKeys.downloadInvoice.tr(context: context), icon: AppAssets.download).onPress(widget.downloadInvoice!), if (widget.trailing != null) widget.trailing!, ] ], @@ -353,8 +332,7 @@ class _ScrollAnimatedTitleState extends State { child: Container( height: isFoldable ? 50.h : 40.h, padding: EdgeInsets.all(8.w), - decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: AppColors.whiteColor, borderRadius: 8.r, side: BorderSide(width: 1, color: AppColors.borderGrayColor)), + decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 8.r, side: BorderSide(width: 1, color: AppColors.borderGrayColor)), child: ShaderMask( blendMode: BlendMode.srcIn, shaderCallback: (bounds) => AppColors.aiLinearGradient.createShader(bounds), diff --git a/lib/widgets/bottom_navigation/bottom_navigation.dart b/lib/widgets/bottom_navigation/bottom_navigation.dart index e999842f..a5b8b239 100644 --- a/lib/widgets/bottom_navigation/bottom_navigation.dart +++ b/lib/widgets/bottom_navigation/bottom_navigation.dart @@ -28,7 +28,13 @@ class BottomNavigation extends StatelessWidget { icon: AppAssets.bookAppoBottom, fillIcon: AppAssets.bookAppoBottom, label: LocaleKeys.appointments.tr(context: context), - iconSize: 32, isSpecial: true), + iconSize: 32, + isSpecial: true, + // Explicit dynamic color — the SVG asset itself is a fixed red, so + // without this override the Appointments icon would never react to + // dark mode or the active occasion theme (e.g. National Day green). + color: AppColors.primaryRedColor, + ), appState.isAuthenticated // ? BottomNavItem(icon: AppAssets.toDoBottom, label: LocaleKeys.todoList.tr(context: context)) ? BottomNavItem(icon: AppAssets.symptomCheckerBottomIcon, fillIcon: AppAssets.symptomCheckerBottomFillIcon, label: LocaleKeys.symptoms.tr(context: context)) @@ -75,7 +81,8 @@ class BottomNavigation extends StatelessWidget { icon: isSelected ? item.fillIcon : item.icon, height: item.iconSize.h, width: item.iconSize.w, - applyThemeColor: !item.isSpecial + applyThemeColor: !item.isSpecial, + iconColor: item.color, // iconColor: isSelected ? Colors.black87 : Colors.black87, ), // const SizedBox(height: 10), diff --git a/lib/widgets/buttons/custom_button.dart b/lib/widgets/buttons/custom_button.dart index 82b62507..8b8e599a 100644 --- a/lib/widgets/buttons/custom_button.dart +++ b/lib/widgets/buttons/custom_button.dart @@ -10,8 +10,8 @@ class CustomButton extends StatefulWidget { final String text; final String? icon; final VoidCallback? onPressed; - final Color backgroundColor; - final Color borderColor; + final Color? backgroundColor; + final Color? borderColor; final Color textColor; final double? borderRadius; final double borderWidth; @@ -33,8 +33,8 @@ class CustomButton extends StatefulWidget { super.key, required this.text, required this.onPressed, - this.backgroundColor = const Color(0xFFED1C2B), - this.borderColor = const Color(0xFFED1C2B), + this.backgroundColor, + this.borderColor, this.textColor = Colors.white, this.borderRadius, this.borderWidth = 2, @@ -93,6 +93,8 @@ class _CustomButtonState extends State with SingleTickerProviderSt final iconS = widget.iconSize ?? (24.h); final fontS = widget.fontSize ?? (16.f); final isButtonDisabled = widget.isDisabled || widget.onPressed == null; + final resolvedBackgroundColor = widget.backgroundColor ?? AppColors.primaryRedColor; + final resolvedBorderColor = widget.borderColor ?? AppColors.primaryRedColor; return Semantics( button: true, @@ -134,10 +136,10 @@ class _CustomButtonState extends State with SingleTickerProviderSt width: widget.width, padding: widget.padding, decoration: RoundedRectangleBorder().toSmoothCornerDecoration( - color: isButtonDisabled ? AppColors.inputLabelTextColor.withValues(alpha: 0.3) : widget.backgroundColor, + color: isButtonDisabled ? AppColors.inputLabelTextColor.withValues(alpha: 0.3) : resolvedBackgroundColor, borderRadius: radius, customBorder: BorderRadius.circular(radius), - side: widget.borderSide ?? BorderSide(width: widget.borderWidth.h, color: isButtonDisabled ? Colors.transparent : widget.borderColor)), + side: widget.borderSide ?? BorderSide(width: widget.borderWidth.h, color: isButtonDisabled ? Colors.transparent : resolvedBorderColor)), child: Row( crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center, diff --git a/lib/widgets/date_range_selector/date_range_calender.dart b/lib/widgets/date_range_selector/date_range_calender.dart index a544f010..28098824 100644 --- a/lib/widgets/date_range_selector/date_range_calender.dart +++ b/lib/widgets/date_range_selector/date_range_calender.dart @@ -163,7 +163,7 @@ class _DateRangeSelectorState extends State { ), startRangeSelectionColor: AppColors.primaryRedColor, endRangeSelectionColor: AppColors.primaryRedColor, - rangeSelectionColor: AppColors.primaryRedColor.withOpacity(0.1), + rangeSelectionColor: AppColors.secondaryLightRedColor, rangeTextStyle: TextStyle( fontFamily: "Poppins", ), @@ -963,7 +963,7 @@ class _DateRangeSelectorState extends State { children: [ AppCustomChipWidget( labelText: LocaleKeys.thisWeek.tr(context: context), - backgroundColor: model.currentlySelectedRange == Range.WEEKLY ? AppColors.primaryRedColor.withOpacity(0.1) : AppColors.whiteColor, + backgroundColor: model.currentlySelectedRange == Range.WEEKLY ? AppColors.secondaryLightRedColor : AppColors.whiteColor, shape: RoundedRectangleBorder( side: BorderSide( color: model.currentlySelectedRange == Range.WEEKLY ? AppColors.primaryRedBorderColor : AppColors.chipBorderColorOpacity20, @@ -977,7 +977,7 @@ class _DateRangeSelectorState extends State { }), AppCustomChipWidget( labelText: LocaleKeys.lastMonth.tr(context: context), - backgroundColor: model.currentlySelectedRange == Range.LAST_MONTH ? AppColors.primaryRedColor.withOpacity(0.1) : AppColors.whiteColor, + backgroundColor: model.currentlySelectedRange == Range.LAST_MONTH ? AppColors.secondaryLightRedColor : AppColors.whiteColor, shape: RoundedRectangleBorder( side: BorderSide( color: model.currentlySelectedRange == Range.LAST_MONTH ? AppColors.primaryRedBorderColor : AppColors.chipBorderColorOpacity20, @@ -991,7 +991,7 @@ class _DateRangeSelectorState extends State { }), AppCustomChipWidget( labelText: LocaleKeys.lastSixMonths.tr(context: context), - backgroundColor: model.currentlySelectedRange == Range.LAST_6MONTH ? AppColors.primaryRedColor.withOpacity(0.1) : AppColors.whiteColor, + backgroundColor: model.currentlySelectedRange == Range.LAST_6MONTH ? AppColors.secondaryLightRedColor : AppColors.whiteColor, shape: RoundedRectangleBorder( side: BorderSide( color: model.currentlySelectedRange == Range.LAST_6MONTH ? AppColors.primaryRedBorderColor : AppColors.chipBorderColorOpacity20, @@ -1012,7 +1012,7 @@ class _DateRangeSelectorState extends State { ], ), labelText: "${LocaleKeys.year.tr(context: context)} ${model.getCurrentYear}", - backgroundColor: model.currentlySelectedRange == Range.THIS_YEAR ? AppColors.primaryRedColor.withOpacity(0.1) : AppColors.whiteColor, + backgroundColor: model.currentlySelectedRange == Range.THIS_YEAR ? AppColors.secondaryLightRedColor : AppColors.whiteColor, shape: RoundedRectangleBorder( side: BorderSide( color: model.currentlySelectedRange == Range.THIS_YEAR ? AppColors.primaryRedBorderColor : AppColors.chipBorderColorOpacity20, diff --git a/lib/widgets/language_switcher.dart b/lib/widgets/language_switcher.dart index 15237363..7a556c84 100644 --- a/lib/widgets/language_switcher.dart +++ b/lib/widgets/language_switcher.dart @@ -56,21 +56,22 @@ class _LanguageSelectorState extends State { child: Row( mainAxisSize: MainAxisSize.min, children: [ - Text( // currentLangData['name']?.toUpperCase() ?? 'EN', widget.currentLanguage == 'ar' ? "ENGLISH" : "العربية", style: context.dynamicTextStyle( fontWeight: FontWeight.w600, - fontSize: 14.f, - color: AppColors.primaryRedColor, - letterSpacing: 0.1, - isLanguageSwitcher: true, + fontSize: 14.f, + color: AppColors.primaryRedColor, + letterSpacing: 0.1, + isLanguageSwitcher: true, fontFamily: widget.currentLanguage == 'ar' ? "Poppins" : "CairoArabic"), ), SizedBox(width: 6.h), - Utils.buildSvgWithAssets(icon: AppAssets.language), - + Utils.buildSvgWithAssets( + icon: AppAssets.language, + iconColor: AppColors.primaryRedColor, + ), ], ), ), diff --git a/lib/widgets/map/HMSMap.dart b/lib/widgets/map/HMSMap.dart index 50da5f3a..e2059a52 100644 --- a/lib/widgets/map/HMSMap.dart +++ b/lib/widgets/map/HMSMap.dart @@ -220,7 +220,7 @@ class _MapTypeItem extends StatelessWidget { padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 8.h), decoration: BoxDecoration( color: isSelected - ? AppColors.primaryRedColor.withOpacity(0.1) + ? AppColors.secondaryLightRedColor : Colors.transparent, borderRadius: BorderRadius.circular(8.r), ), diff --git a/lib/widgets/map/gms_map.dart b/lib/widgets/map/gms_map.dart index 33e0befb..07bf2674 100644 --- a/lib/widgets/map/gms_map.dart +++ b/lib/widgets/map/gms_map.dart @@ -372,7 +372,7 @@ class _MapTypeItem extends StatelessWidget { padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 8.h), decoration: BoxDecoration( color: isSelected - ? AppColors.primaryRedColor.withOpacity(0.1) + ? AppColors.secondaryLightRedColor : Colors.transparent, borderRadius: BorderRadius.circular(8.r), ), diff --git a/pubspec.lock b/pubspec.lock index 2aeaaac9..70ddfc04 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -486,6 +486,30 @@ packages: url: "https://pub.dev" source: hosted version: "3.10.10" + firebase_remote_config: + dependency: "direct main" + description: + name: firebase_remote_config + sha256: e1635b1e8713f4a823920ec3a56a14034b90ce455d47746ab0da994857f370cf + url: "https://pub.dev" + source: hosted + version: "5.5.0" + firebase_remote_config_platform_interface: + dependency: transitive + description: + name: firebase_remote_config_platform_interface + sha256: ce836c5c62056edbe23ef501e6876691ee32476afd12fe95b76e57bba9d25485 + url: "https://pub.dev" + source: hosted + version: "2.0.0" + firebase_remote_config_web: + dependency: transitive + description: + name: firebase_remote_config_web + sha256: "9dbd75024bfcd47c05046c95f9cf648a8fc862b096bcf8ea1e4b855d50ea19ad" + url: "https://pub.dev" + source: hosted + version: "1.8.9" fixnum: dependency: transitive description: @@ -797,10 +821,10 @@ packages: dependency: "direct main" description: name: flutter_zoom_videosdk - sha256: "46a4dea664b1c969099328a499c198a1755adf9ac333dea28bea5187910b3bf9" + sha256: "87caad07b994f09b9d8c4b8054c0dff5659029abae38dd6ec223460c3f1bcb7d" url: "https://pub.dev" source: hosted - version: "2.1.10" + version: "2.5.10" fluttertoast: dependency: "direct main" description: @@ -1409,6 +1433,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" + myfatoorah_flutter: + dependency: "direct main" + description: + name: myfatoorah_flutter + sha256: c9fa65c6329f3c076e48421726b855a444d9790b480d50822452f14f0e56d95f + url: "https://pub.dev" + source: hosted + version: "3.3.2" ndef: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 8cd63b67..c7e8ba1d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -70,6 +70,7 @@ dependencies: google_api_availability: ^5.0.1 firebase_analytics: ^11.5.1 firebase_crashlytics: ^4.3.8 + firebase_remote_config: ^5.5.0 jiffy: ^6.4.3 # hijri_gregorian_calendar: # path: /Users/aamir/StudioProjects/hijri_gregorian_calendar @@ -113,6 +114,7 @@ dependencies: cloudflare_turnstile: ^3.7.2 freerasp: ^8.2.1 safe_device: ^1.4.1 + myfatoorah_flutter: ^3.3.2 dev_dependencies: flutter_test: