diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 6d2f83f..497d14a 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -4,6 +4,7 @@ + diff --git a/assets/icons/ic_add_dependents.png b/assets/icons/ic_add_dependents.png new file mode 100644 index 0000000..5202f1f Binary files /dev/null and b/assets/icons/ic_add_dependents.png differ diff --git a/assets/icons/ic_category_check.png b/assets/icons/ic_category_check.png new file mode 100644 index 0000000..94de67b Binary files /dev/null and b/assets/icons/ic_category_check.png differ diff --git a/assets/images/all_categories.png b/assets/images/all_categories.png new file mode 100644 index 0000000..aa6f895 Binary files /dev/null and b/assets/images/all_categories.png differ diff --git a/assets/images/cat_all.png b/assets/images/cat_all.png new file mode 100644 index 0000000..aa6f895 Binary files /dev/null and b/assets/images/cat_all.png differ diff --git a/assets/langs/ar-SA.json b/assets/langs/ar-SA.json index 89a6568..7878074 100644 --- a/assets/langs/ar-SA.json +++ b/assets/langs/ar-SA.json @@ -381,7 +381,7 @@ "browseCategories": "تصفح الفئات", "searchItems": "عناصر البحث", "offerAndDiscounts": "العروض والخصومات", - "offerValid": "العرض صالح", + "offerValid": "العرض ساري", "offerExpired": "انتهى العرض", "whatAreYouOffering": "ما الذي تعرضه؟", "selectCategory": "اختر الفئة", @@ -552,9 +552,6 @@ "pleaseClickButtonToJoinMarathon": "الرجاء الضغط على الزر أدناه للانضمام إلى الماراثون", "youCannotJoinTheMarathon": "لا يمكنك الانضمام إلى الماراثون لأنك تجاوزت الحد الزمني", "open": "يفتح", - "youCannotJoinTheMarathon": "لا يمكنك الانضمام إلى الماراثون لأنك تجاوزت الحد الزمني", - "pleaseClickButtonToJoinMarathon": "الرجاء الضغط على الزر أدناه للانضمام إلى الماراثون", - "generate": "يولد", "paymentRequest": "طلب الدفع", "paymentDetails": "تفاصيل الدفع", "requestNo": "رقم الطلب", @@ -669,16 +666,117 @@ "incidentDetails": "تفاصيل الحادث", "ticketNumber": "رقم التذكرة", "ticketId": "معرف التذكرة", - "pleaseSelectProject": "الرجاء اختيار مشروع", - "viewAttachment" : "عرض المرفق", - "parkingQr": "وقوف السيارات QR", - "startNow": "ابدأ الآن", - "destination_type": "نوع الوجهة", - "source_organization_name": "اسم منظمة المصدر", - "destination_organization_name": "اسم منظمة الوجهة", + "pleaseSelectProject": "الرجاء اختيار مشروع", + "viewAttachment": "عرض المرفق", + "parkingQr": "وقوف السيارات QR", + "startNow": "ابدأ الآن", + "destination_type": "نوع الوجهة", + "source_organization_name": "اسم منظمة المصدر", + "destination_organization_name": "اسم منظمة الوجهة", "costCenter": "مركز التكلفة", "priceSAR": "السعر (ريال سعودي)", "amountSAR": "المبلغ (ريال سعودي)", - "amuLastThreeMonths": "AMU (آخر 3 أشهر)" + "amuLastThreeMonths": "AMU (آخر 3 أشهر)", + "mazayaTitle": "مزايا", + "mazayaAccessGranted": "تم منح حق الوصول لمزايا", + "mazayaAccessGrantedDesc": "أصبح لدى {} حق وصول نشط الآن. شارك رابط التنزيل أدناه ليتمكن من تثبيت تطبيق مهم وإكمال الإعداد.", + "accessIsNowActive": "الوصول نشط الآن", + "accessIsNowActiveDesc": "يمكن لـ {} استخدام الرابط المرفق لتنزيل تطبيق مزايا ومتابعة الإعداد على جهازه.", + "dependentAccessActive": "وصول التابع نشط", + "shareableAppDownloadLink": "رابط تنزيل التطبيق للمشاركة", + "shareableAppDownloadLinkDesc": "أرسل هذا الرابط إلى {} ليتمكن من تنزيل تطبيق مزايا ومتابعة الإعداد.", + "downloadLink": "رابط التنزيل", + "shareDownloadLink": "مشاركة رابط التنزيل", + "copyLink": "نسخ الرابط", + "done": "تم", + "linkCopied": "تم نسخ الرابط بنجاح", + "mazayaAccessRevoked": "تم إلغاء الوصول لمزايا", + "mazayaAccessRevokedDesc": "قام الكفيل بإلغاء وصول التابع. لم يعد بالإمكان فتح العروض أو تسجيل الدخول بالبصمة.", + "contactYourSponsor": "تواصل مع الكفيل", + "contactYourSponsorDesc": "اطلب من كفيلك السماح بالوصول لمزايا مرة أخرى من تفاصيل التابعين. يمكن للكفيل فقط استعادة الوصول.", + "dependentAccessBlocked": "تم حظر وصول التابع", + "returnToSignIn": "العودة إلى تسجيل الدخول", + "allowMazayaAccess": "السماح بالوصول لمزايا", + "revokeMazayaAccess": "إلغاء الوصول لمزايا", + "active": "نشط", + "noAccess": "غير متاح", + "offerDetails": "تفاصيل العرض", + "offerIdNotAvailable": "معرف العرض غير متوفر", + "unableToRedeemOffer": "تعذر الحصول على رابط العرض، يرجى المحاولة لاحقاً", + "errorRedeemingOffer": "حدث خطأ أثناء استخدام العرض", + "useOffer": "استخدام العرض", + "online": "عبر الإنترنت", + "inStore": "في المتجر", + "ksa": "المملكة", + "riyadh": "الرياض", + "noInstructionsAvailable": "لا توجد طريقة استخدام محددة.", + "contactInformation": "معلومات التواصل:", + "humanResources": "الموارد البشرية", + "drSulaimanAlHabib": "سليمان الحبيب", + "allCategories": "الكل", + "noOffersAvailable": "لا توجد عروض متاحة", + "addDependents": "إضافة تابعين", + "myDependents": "التابعون", + "offDiscount": "خصم {}", + "usageLabel": "الاستخدام: ", + "areYouSureWantToLogOut": "هل أنت متأكد من تسجيل الخروج؟", + "searchDependents": "البحث في التابعين", + "searchFamilyMembers": "البحث في أفراد العائلة", + "addAnotherUser": "إضافة مستخدم آخر", + "joinMazaya": "انضم إلى مزايا", + "joinMazayaSubtitle": "احصل على أفضل العروض والصفقات الحصرية في المطاعم والتسوق ومكافآت نمط الحياة.", + "continueAsEmployee": "المتابعة كموظف", + "continueAsEmployeeSubtitle": "سجل الدخول باستخدام اسم المستخدم وكلمة المرور الخاصة بالموظف.", + "pleaseEnterNationalId": "الرجاء إدخال رقم الهوية الوطنية", + "pleaseSelectDob": "الرجاء تحديد تاريخ الميلاد", + "unableToRetrieveDependentInfo": "تعذر الحصول على معلومات التابع", + "failedToSendOtp": "فشل إرسال رمز التحقق", + "verificationCodeResent": "تم إعادة إرسال رمز التحقق", + "nationalId": "الهوية الوطنية", + "addDependent": "إضافة تابع", + "stepVerify": "التحقق", + "stepBasic": "الأساسية", + "stepAdditional": "إضافية", + "findYourDependent": "البحث عن تابعك", + "findYourDependentDesc": "أدخل التفاصيل تماماً كما تظهر في الهوية الوطنية. سنقوم باسترداد السجل بأمان من مركز المعلومات الوطني (NHIC).", + "enter10DigitNationalId": "أدخل رقم الهوية المكون من 10 أرقام", + "nationalIdMustBe10Digits": "يجب أن يتكون رقم الهوية من 10 أرقام", + "dateFormatPlaceholder": "يوم / شهر / سنة", + "searchUser": "بحث عن المستخدم", + "dependentNotFoundNhic": "لم يتم العثور على سجل لهذا التابع في مركز المعلومات الوطني", + "basicInfo": "المعلومات الأساسية", + "basicInfoDesc": "بيانات الهوية المطلوبة المستردة من مركز المعلومات الوطني.", + "additionalInfo": "معلومات إضافية", + "additionalInfoDesc": "إضافة أو مراجعة بيانات الكفالة والأهلية.", + "select": "اختر", + "enterFieldPlaceholder": "أدخل {}", + "requiredField": "الحقل المطلوب", + "pleaseFillField": "الرجاء إدخال {}", + "reviewAndSubmit": "المراجعة والإرسال", + "reviewAndSubmitDesc": "يرجى مراجعة تفاصيل التابع بعناية قبل تأكيد الإرسال.", + "arabicName": "الاسم بالعربية", + "relationship": "صلة القرابة", + "sponsor": "الكفيل", + "dependentSponsorDetail": "تفاصيل كفيل التابع", + "insuranceEligibility": "أهلية التأمين", + "relationshipStartDate": "تاريخ بدء العلاقة", + "dependentAddedSuccessfully": "تم إرسال الطلب بنجاح", + "genderMale": "ذكر", + "genderFemale": "أنثى", + "religionIslam": "الإسلام", + "back": "السابق", + "submitRequest": "إرسال الطلب", + "enterDependentDetailsToContinue": "أدخل تفاصيل التابع للمتابعة.", + "enterBasicDetailsDesc": "يرجى إدخال البيانات الأساسية للتابع.", + "locations": "الفروع والمواقع", + "availableOffers": "العروض المتاحة", + "selectOfferToRedeem": "اختر العرض الذي ترغب في استخدامه", + "viewOnMap": "عرض على الخريطة", + "showAllLocations": "عرض جميع الفروع ({})", + "showLess": "عرض أقل", + "searchBranches": "البحث عن الفروع أو المدن...", + "allCities": "الكل", + "noBranchesFound": "لم يتم العثور على فروع مطابقة", + "browseAllBranches": "تصفح جميع الفروع ({}) في مختلف المدن", + "branchesCount": "{} فرع" } - diff --git a/assets/langs/en-US.json b/assets/langs/en-US.json index 306620b..6e194e9 100644 --- a/assets/langs/en-US.json +++ b/assets/langs/en-US.json @@ -570,7 +570,6 @@ "pleaseClickButtonToJoinMarathon": "Press the button below to join the Marathon.", "youCannotJoinTheMarathon": "You cannot join the Marathon because you have exceeded the time limit.", "requesterOperatingUnit": "Requester Operating Unit", - "generate": "Generate", "paymentRequest": "Pay Request", "paymentDetails": "Payment Details", "requestNo": "Request No", @@ -667,15 +666,117 @@ "incidentDetails": "Incident Details", "ticketNumber": "Ticket Number", "ticketId": "Ticket ID", - "pleaseSelectProject": "Please select a project", - "viewAttachment" : "View Attachment", - "parkingQr": "Parking QR", - "startNow": "Start Now", - "destination_type": "Destination Type", - "source_organization_name": "Source Organization Name", + "pleaseSelectProject": "Please select a project", + "viewAttachment": "View Attachment", + "parkingQr": "Parking QR", + "startNow": "Start Now", + "destination_type": "Destination Type", + "source_organization_name": "Source Organization Name", "destination_organization_name": "Destination Organization Name", "costCenter": "Cost Center", "priceSAR": "Price (SAR)", "amountSAR": "Amount (SAR)", - "amuLastThreeMonths": "AMU (Last 3 months)" -} \ No newline at end of file + "amuLastThreeMonths": "AMU (Last 3 months)", + "mazayaTitle": "Mazaya", + "mazayaAccessGranted": "Mazaya access granted", + "mazayaAccessGrantedDesc": "{} now has active access. Share the download link below so they can install the Mohem app and complete setup.", + "accessIsNowActive": "Access is now active", + "accessIsNowActiveDesc": "{} can use the provided link to download the Mazaya app and continue setup on their device.", + "dependentAccessActive": "Dependent access active", + "shareableAppDownloadLink": "Shareable app-download link", + "shareableAppDownloadLinkDesc": "Send this link to {} so they can download the Mazaya app and continue setup.", + "downloadLink": "Download link", + "shareDownloadLink": "Share Download Link", + "copyLink": "Copy Link", + "done": "Done", + "linkCopied": "Link copied to clipboard", + "mazayaAccessRevoked": "Mazaya access revoked", + "mazayaAccessRevokedDesc": "Your sponsor has revoked your dependent access. You can no longer open offers or sign in with biometrics.", + "contactYourSponsor": "Contact your sponsor", + "contactYourSponsorDesc": "Ask your sponsor to allow Mazaya access again from Dependent Details. Only the sponsor can restore access.", + "dependentAccessBlocked": "Dependent access blocked", + "returnToSignIn": "Return to Sign In", + "allowMazayaAccess": "Allow Mazaya Access", + "revokeMazayaAccess": "Revoke Mazaya Access", + "active": "Active", + "noAccess": "No access", + "offerDetails": "Offer Details", + "offerIdNotAvailable": "Offer ID not available", + "unableToRedeemOffer": "Unable to redeem offer, please try again later", + "errorRedeemingOffer": "An error occurred while redeeming offer", + "useOffer": "Use Offer", + "online": "Online", + "inStore": "In Store", + "ksa": "KSA", + "riyadh": "Riyadh", + "noInstructionsAvailable": "No instructions available.", + "contactInformation": "Contact Information:", + "humanResources": "Human Resources", + "drSulaimanAlHabib": "DR SULAIMAN AL HABIB", + "allCategories": "All", + "noOffersAvailable": "No offers available", + "addDependents": "Add Dependents", + "myDependents": "My Dependents", + "offDiscount": "{} Off", + "usageLabel": "Usage: ", + "areYouSureWantToLogOut": "Are you sure you want to log out?", + "searchDependents": "Search dependents", + "searchFamilyMembers": "Search family members", + "addAnotherUser": "Add another user", + "joinMazaya": "Join Mazaya", + "joinMazayaSubtitle": "Get the best offers and exclusive deals across dining, shopping, and lifestyle rewards.", + "continueAsEmployee": "Continue as an employee", + "continueAsEmployeeSubtitle": "Sign in with your employee username and password.", + "pleaseEnterNationalId": "Please enter National ID", + "pleaseSelectDob": "Please select Date of Birth", + "unableToRetrieveDependentInfo": "Unable to retrieve dependent information", + "failedToSendOtp": "Failed to send verification code", + "verificationCodeResent": "Verification code resent", + "nationalId": "National ID", + "addDependent": "Add Dependent", + "stepVerify": "Verify", + "stepBasic": "Basic", + "stepAdditional": "Additional", + "findYourDependent": "Find your dependent", + "findYourDependentDesc": "Enter the details exactly as they appear on the National ID. We'll securely retrieve the record from NHIC.", + "enter10DigitNationalId": "Enter 10-digit National ID", + "nationalIdMustBe10Digits": "National ID must be 10 digits", + "dateFormatPlaceholder": "DD / MM / YYYY", + "searchUser": "Search User", + "dependentNotFoundNhic": "Sorry, error while getting dependent info from NHIC", + "basicInfo": "Basic Info", + "basicInfoDesc": "Required identity details fetched from NHIC.", + "additionalInfo": "Additional Info", + "additionalInfoDesc": "Add or review sponsorship and eligibility details.", + "select": "Select", + "enterFieldPlaceholder": "Enter {}", + "requiredField": "Required field", + "pleaseFillField": "Please fill {}", + "reviewAndSubmit": "Review & Submit", + "reviewAndSubmitDesc": "Please review the dependent details carefully before submitting.", + "arabicName": "Arabic Name", + "relationship": "Relationship", + "sponsor": "Sponsor", + "dependentSponsorDetail": "Dependent Sponsor Detail", + "insuranceEligibility": "Insurance Eligibility", + "relationshipStartDate": "Start Date", + "dependentAddedSuccessfully": "Dependent added successfully", + "genderMale": "Male", + "genderFemale": "Female", + "religionIslam": "Islam", + "back": "Back", + "submitRequest": "Submit", + "enterDependentDetailsToContinue": "Enter the dependent details to continue.", + "enterBasicDetailsDesc": "Please enter the dependent's basic identity details.", + "locations": "Locations", + "availableOffers": "Available Offers", + "selectOfferToRedeem": "Select an offer to use", + "viewOnMap": "View on map", + "showAllLocations": "Show all {} locations", + "showLess": "Show less", + "searchBranches": "Search branches or cities...", + "allCities": "All", + "noBranchesFound": "No branches found", + "browseAllBranches": "Browse all {} branches across cities", + "branchesCount": "{} Branches" +} diff --git a/lib/api/api_client.dart b/lib/api/api_client.dart index 7e346c8..edce588 100644 --- a/lib/api/api_client.dart +++ b/lib/api/api_client.dart @@ -88,7 +88,7 @@ class ApiClient { if (!kReleaseMode) { print("Url:$url"); var bodyJson = json.encode(jsonObject); - print("body:$bodyJson"); + print("body:$bodyJson" ); } var response = await postJsonForResponse(url, jsonObject, token: token, queryParameters: queryParameters, headers: _headers, retryTimes: retryTimes, isFormData: isFormData); try { diff --git a/lib/api/login_api_client.dart b/lib/api/login_api_client.dart index f487b80..22ecc7f 100644 --- a/lib/api/login_api_client.dart +++ b/lib/api/login_api_client.dart @@ -1,13 +1,16 @@ import 'dart:async'; import 'dart:io'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:mohem_flutter_app/api/api_client.dart'; import 'package:mohem_flutter_app/app_state/app_state.dart'; import 'package:mohem_flutter_app/classes/consts.dart'; +import 'package:mohem_flutter_app/classes/utils.dart'; import 'package:mohem_flutter_app/models/basic_member_information_model.dart'; import 'package:mohem_flutter_app/models/check_mobile_app_version_model.dart'; import 'package:mohem_flutter_app/models/generic_response_model.dart'; +import 'package:mohem_flutter_app/models/dependent/get_mobile_login_info_dependent_model.dart'; import 'package:mohem_flutter_app/models/get_mobile_login_info_list_model.dart'; import 'package:mohem_flutter_app/models/member_login_list_model.dart'; @@ -30,6 +33,50 @@ class LoginApiClient { }, url, postParams); } + Future getMobileLoginInfoNEWDependent(String deviceToken, String deviceType, {int? memberType}) async { + String url = "${ApiConsts.erpRest}Mohemm_GetMobileLoginInfo_NEW_Dependent"; + String effectiveDeviceToken = deviceToken; + if (effectiveDeviceToken.isEmpty) { + effectiveDeviceToken = AppState().getDeviceToken ?? ""; + } + // if (effectiveDeviceToken.isEmpty && !AppState().getIsHuawei) { + // try { + // await Firebase.initializeApp(); + // String? tok = await FirebaseMessaging.instance.getToken(); + // if (tok != null && tok.isNotEmpty) { + // effectiveDeviceToken = tok; + // AppState().setDeviceToken = tok; + // } + // } catch (_) {} + // } + + Map postParams = {}; + postParams["DeviceToken"] = effectiveDeviceToken; + postParams["DeviceType"] = deviceType; + postParams["MemberType"] = memberType ?? MemberType.dependent.value; + return await ApiClient().postJsonForObject((json) { + if (kDebugMode) { + print("Mohemm_GetMobileLoginInfo_NEW_Dependent Response: $json"); + } + List? list = json['Mohemm_GetMobileLoginInfo_NEW_DependentList'] ?? + json['Mohemm_GetMobileLoginInfo_NEW_Dependent'] ?? + json['Mohemm_GetMobileLoginInfo_DependentList'] ?? + json['Mohemm_GetMobileLoginInfoDependentList'] ?? + json['Mohemm_GetMobileLoginInfoList'] ?? + json['GetMobileLoginInfoList'] ?? + json['Response'] ?? + json['Data']; + if (list != null && list.isNotEmpty && list.first is Map) { + return GetMobileLoginInfoDependentModel.fromJson(list.first as Map); + } + if (json is Map && + (json['DependentID'] != null || json['DepedentName'] != null || json['DOB'] != null || json['NationalID'] != null || json['LoginType'] != null)) { + return GetMobileLoginInfoDependentModel.fromJson(json); + } + return null; + }, url, postParams); + } + Future insertMobileLoginInfoNEW( String email, int sessionId, String employeeName, int loginType, String mobileNumber, String userName, String deviceToken, String deviceType, {int? memberType}) async { @@ -56,6 +103,71 @@ class LoginApiClient { }, url, postParams); } + Future insertMobileLoginInfoDependent({ + required int loginType, + String? nationalId, + String? dob, + String? dependentName, + String? mobileNumber, + String? logInTokenID, + String? tokenID, + int? sessionId, + String? email, + String? deviceToken, + String? deviceType, + int? companyId, + int? versionId, + int? channel, + int? memberType, + BuildContext? context, + }) async { + String url = "${ApiConsts.erpRest}Mohemm_InsertMobileLoginInfo_Dependent"; + String devType = deviceType ?? (Platform.isAndroid ? "android" : "ios"); + String devToken = (deviceToken != null && deviceToken.isNotEmpty) + ? deviceToken + : (AppState().getIsHuawei ? AppState().getHuaweiPushToken : (AppState().getDeviceToken ?? "")); + + String effectiveToken = (tokenID != null && tokenID.isNotEmpty) + ? tokenID + : (AppState().postParamsObject?.tokenID ?? AppState().authenticationTokenID ?? ""); + + Map postParams = { + "VersionID": versionId ?? 5, + "Channel": channel ?? 31, + "LanguageID": context != null ? AppState().getLanguageID(context) : 2, + "MemberType": memberType ?? 1, + "MobileType": devType, + "MobileNumber": mobileNumber ?? "", + "UserName": nationalId ?? "", + "P_USER_NAME": nationalId ?? "", + "P_DEP_NATIONAL_ID": nationalId ?? "", + "P_DEP_BIRTH_DATE": dob ?? "", + "P_DEPENDENT_NAME": dependentName ?? "", + "EmployeeName": dependentName ?? "", + "LogInTokenID": logInTokenID ?? (AppState().postParamsObject?.logInTokenID ?? ""), + "CompanyID": companyId ?? 1, + "DeviceType": devType, + "DeviceToken": devToken, + "LoginType": loginType, + "P_SESSION_ID": sessionId ?? (AppState().postParamsObject?.pSessionId ?? 0), + "TokenID": effectiveToken, + "P_EMAIL_ADDRESS": email ?? "", + }; + + if (kDebugMode) { + print("Mohemm_InsertMobileLoginInfo (Dependent) Request: $postParams"); + } + + return await ApiClient().postJsonForObject((json) { + if (kDebugMode) { + print("Mohemm_InsertMobileLoginInfo (Dependent) Response: $json"); + } + GenericResponseModel? responseData = GenericResponseModel.fromJson(json); + AppState().setLogged = true; + return responseData; + }, url, postParams); + } + Future checkMobileAppVersion() async { String url = "${ApiConsts.utilitiesRest}CheckMobileAppVersion"; Map postParams = {}; @@ -102,6 +214,7 @@ class LoginApiClient { AppState().postParamsObject?.pUserName = AppState().getUserName; AppState().postParamsObject?.pSelectedEmployeeNumber = AppState().getUserName; AppState().postParamsObject?.setPLegislationCode = responseData.basicMemberInformation!.pLEGISLATIONCODE; + AppState().postParamsInitConfig.setPLegislationCode = responseData.basicMemberInformation!.pLEGISLATIONCODE; AppState().postParamsObject?.setPayrollCodeStr = responseData.memberInformationList!.first.pAYROLLCODE; AppState().setBusinessCardPrivilege = responseData.businessCardPrivilege ?? false; return responseData; @@ -178,22 +291,62 @@ class LoginApiClient { Future memberLoginDependent({ required String nationalId, required String birthDate, + String? deviceToken, + String? deviceType, BuildContext? context, }) async { String url = "${ApiConsts.erpRest}MemberLogin_Dependent"; + String devType = deviceType ?? (Platform.isAndroid ? "android" : "ios"); + String devToken = deviceToken ?? ""; + if (devToken.isEmpty) { + devToken = AppState().getIsHuawei + ? AppState().getHuaweiPushToken + : (AppState().getDeviceToken ?? ""); + } + if (devToken.isEmpty && !AppState().getIsHuawei) { + try { + String savedToken = await Utils.getStringFromPrefs("P_DEP_DEVICE_TOKEN"); + if (savedToken.isNotEmpty) { + devToken = savedToken; + } + } catch (_) {} + } + Map postParams = { "VersionID": 20, "Channel": 31, "LanguageID": context != null ? AppState().getLanguageID(context) : 2, - "MobileType": Platform.isAndroid ? "android" : "ios", + "DeviceToken": devToken, + "DeviceType": devType, + "MobileType": devType, "P_DEP_NATIONAL_ID": nationalId, "P_DEP_BIRTH_DATE": birthDate, }; + if (kDebugMode) { + print("MemberLogin_Dependent Request: $postParams"); + } return await ApiClient().postJsonForObject((json) { + if (kDebugMode) { + print("MemberLogin_Dependent Response: $json"); + } GenericResponseModel responseData = GenericResponseModel.fromJson(json); if (responseData.logInTokenID != null && responseData.logInTokenID!.isNotEmpty) { AppState().postParamsObject?.setLogInTokenID = responseData.logInTokenID; } + String? authToken = (responseData.authenticationTokenID != null && responseData.authenticationTokenID!.trim().isNotEmpty) + ? responseData.authenticationTokenID!.trim() + : null; + if (authToken != null && authToken.isNotEmpty) { + AppState().postParamsObject?.setTokenID = authToken; + AppState().setAuthenticationTokenID = authToken; + } + if (responseData.getWalaplusDepInfoModel != null) { + AppState().setWalaplusDepInfoModel = responseData.getWalaplusDepInfoModel; + if (responseData.getWalaplusDepInfoModel?.pDependentName != null && + responseData.getWalaplusDepInfoModel!.pDependentName!.trim().isNotEmpty) { + AppState().setDependentName = responseData.getWalaplusDepInfoModel!.pDependentName!.trim(); + } + } AppState().setUserName = nationalId; AppState().postParamsObject?.setUserName = nationalId; AppState().postParamsObject?.setPUserName = nationalId; @@ -249,13 +402,12 @@ class LoginApiClient { }; return await ApiClient().postJsonForObject((json) { GenericResponseModel responseData = GenericResponseModel.fromJson(json); - String effectiveToken = (responseData.tokenID != null && responseData.tokenID!.isNotEmpty) - ? responseData.tokenID! - : ((responseData.logInTokenID != null && responseData.logInTokenID!.isNotEmpty) - ? responseData.logInTokenID! - : logInTokenID); + String effectiveToken = (responseData.authenticationTokenID != null && responseData.authenticationTokenID!.trim().isNotEmpty) + ? responseData.authenticationTokenID!.trim() + : (AppState().postParamsObject?.tokenID ?? AppState().authenticationTokenID ?? ""); AppState().postParamsObject?.setTokenID = effectiveToken; + AppState().setAuthenticationTokenID = effectiveToken; AppState().postParamsObject?.setLogInTokenID = (responseData.logInTokenID != null && responseData.logInTokenID!.isNotEmpty) ? responseData.logInTokenID! : logInTokenID; @@ -268,6 +420,14 @@ class LoginApiClient { AppState().isAuthenticated = true; AppState().setLogged = true; + if (responseData.getWalaplusDepInfoModel != null) { + AppState().setWalaplusDepInfoModel = responseData.getWalaplusDepInfoModel; + if (responseData.getWalaplusDepInfoModel?.pDependentName != null && + responseData.getWalaplusDepInfoModel!.pDependentName!.trim().isNotEmpty) { + AppState().setDependentName = responseData.getWalaplusDepInfoModel!.pDependentName!.trim(); + } + } + return responseData; }, url, postParams); } diff --git a/lib/api/my_attendance_api_client.dart b/lib/api/my_attendance_api_client.dart index 3978c2d..cd564f8 100644 --- a/lib/api/my_attendance_api_client.dart +++ b/lib/api/my_attendance_api_client.dart @@ -55,9 +55,9 @@ class MyAttendanceApiClient { }, url, postParams); } - Future getCEIDffStructure(String pFunctionName, String? empID) async { + Future getCEIDffStructure(String pFunctionName, String? empID, int? relationID) async { String url = "${ApiConsts.erpRest}GET_CEI_DFF_STRUCTURE"; - Map postParams = {"P_FUNCTION_NAME": pFunctionName}; + Map postParams = {"P_FUNCTION_NAME": pFunctionName,"P_CONTACT_RELATIONSHIP_ID": relationID}; postParams.addAll(AppState().postParamsJson); if (empID != null) postParams["P_SELECTED_EMPLOYEE_NUMBER"] = empID; return await ApiClient().postJsonForObject((json) { diff --git a/lib/api/offers_and_discounts_api_client.dart b/lib/api/offers_and_discounts_api_client.dart index 7fb07ac..bd4723d 100644 --- a/lib/api/offers_and_discounts_api_client.dart +++ b/lib/api/offers_and_discounts_api_client.dart @@ -72,44 +72,116 @@ class OffersAndDiscountsApiClient { ); } - Future> getWalaPlusCategories({BuildContext? context}) async { - String url = ApiConsts.walaPlusCategories; - + String _resolveUserName() { String? pUserName = AppState().getUserName; if (pUserName == null || pUserName.isEmpty) { - pUserName = "2537367522"; + pUserName = AppState().postParamsObject?.userName ?? + AppState().postParamsObject?.pUserName ?? + AppState().memberInformationList?.eMPLOYEENUMBER; } + return pUserName ?? ""; + } - String? tokenID = AppState().postParamsObject?.tokenID; - if (tokenID == null || tokenID.isEmpty || tokenID == "adm!n") { - tokenID = "@dm!n"; + String _resolveDepNationalId() { + if (!ApiConsts.isDependetLogin) { + return ""; + } + return AppState().getUserName ?? ""; + } + + String _resolveMobileNumber({String? overrideMobile}) { + if (overrideMobile != null && overrideMobile.trim().isNotEmpty) { + return overrideMobile.trim(); + } + String? mobile = AppState().postParamsObject?.mobileNumber; + if (mobile != null && mobile.trim().isNotEmpty) { + return mobile.trim(); + } + mobile = AppState().memberInformationList?.eMPLOYEEMOBILENUMBER; + if (mobile != null && mobile.trim().isNotEmpty) { + return mobile.trim(); + } + mobile = AppState().walaplusDepInfoModel?.pEmpMobileNumber; + if (mobile != null && mobile.trim().isNotEmpty) { + return mobile.trim(); } + mobile = AppState().memberLoginList?.pMOBILENUMBER; + if (mobile != null && mobile.trim().isNotEmpty) { + return mobile.trim(); + } + mobile = AppState().memberInformationList?.mobileNumberWithZipCode; + if (mobile != null && mobile.trim().isNotEmpty) { + return mobile.trim(); + } + return ""; + } - String? logInTokenID = AppState().postParamsObject?.logInTokenID; - if (logInTokenID == null || logInTokenID.isEmpty) { - logInTokenID = "@dm!n"; + + Map _getBaseWalaPlusParams() { + Map base = {}; + base.addAll(AppState().postParamsJson); + if (ApiConsts.isDependetLogin) { + String? token = AppState().postParamsObject?.tokenID ?? AppState().authenticationTokenID; + if (token != null && token.isNotEmpty) { + base["TokenID"] = token; + } + String? logInToken = AppState().postParamsObject?.logInTokenID; + if (logInToken != null && logInToken.isNotEmpty) { + base["LogInTokenID"] = logInToken; + } } + _cleanWalaPlusParams(base); + return base; + } - Map postParams = { - "VersionID": 20, - "Channel": 31, - "LanguageID": context != null ? AppState().getLanguageID(context) : 2, - "LogInTokenID": logInTokenID, - "TokenID": tokenID, - "P_USER_NAME": pUserName, - }; + void _cleanWalaPlusParams(Map map) { + map.remove("P_EMAIL_ADDRESS"); + map.remove("P_SESSION_ID"); + map.remove("PayrollCodeStr"); + map.remove("LegislationCodeStr"); + map.remove("P_SELECTED_EMPLOYEE_NUMBER"); + map.removeWhere((key, value) => value == null); + } + + Future> getWalaPlusCategories({BuildContext? context, String? mobileNumber}) async { + String url = ApiConsts.walaPlusCategories; + String pUserName = _resolveUserName(); + String pMobileNumber = _resolveMobileNumber(overrideMobile: mobileNumber); + String pDepNationalId = _resolveDepNationalId(); + + Map postParams = _getBaseWalaPlusParams(); + postParams["UserName"] = pUserName; + postParams["P_USER_NAME"] = pUserName; + postParams["P_DEP_NATIONAL_ID"] = pDepNationalId; + if (pMobileNumber.isNotEmpty) { + postParams["MobileNumber"] = pMobileNumber; + } + postParams["MemberType"] = ApiConsts.isDependetLogin ? 2 : 1; + postParams["CompanyID"] = 1; + _cleanWalaPlusParams(postParams); return await ApiClient().postJsonForObject( (response) { List categories = []; - if (response != null && response['WalaPlus_Categories'] != null) { - var cats = response['WalaPlus_Categories']; + if (response != null) { + dynamic cats = response['WalaPlus_Categories'] ?? + response['walaPlus_Categories'] ?? + response['WalaPlusCategories'] ?? + response['result']; if (cats is String) { - cats = json.decode(cats); + try { + cats = json.decode(cats); + } catch (e) { + debugPrint("Error decoding WalaPlus_Categories: $e"); + } } if (cats is List) { cats.forEach((v) { - categories.add(WalaPlusCategoryModel.fromJson(v)); + if (v is Map) { + categories.add(WalaPlusCategoryModel.fromJson(v)); + } else if (v is Map) { + categories.add(WalaPlusCategoryModel.fromJson(Map.from(v))); + } }); } } @@ -126,51 +198,61 @@ class OffersAndDiscountsApiClient { int perPage = 10, String? geoLocation, BuildContext? context, + String? mobileNumber, }) async { String url = ApiConsts.walaPlusRetailers; - - String? pUserName = AppState().getUserName; - if (pUserName == null || pUserName.isEmpty) { - pUserName = "2537367522"; + String pUserName = _resolveUserName(); + String pMobileNumber = _resolveMobileNumber(overrideMobile: mobileNumber); + String pDepNationalId = _resolveDepNationalId(); + String effectiveCatId = (categoryId == "all" || categoryId == "0" || categoryId == null) ? "" : categoryId; + + Map postParams = _getBaseWalaPlusParams(); + postParams["UserName"] = pUserName; + postParams["P_USER_NAME"] = pUserName; + postParams["P_DEP_NATIONAL_ID"] = pDepNationalId; + if (pMobileNumber.isNotEmpty) { + postParams["MobileNumber"] = pMobileNumber; } - - String? tokenID = AppState().postParamsObject?.tokenID; - if (tokenID == null || tokenID.isEmpty) { - tokenID = "adm!n"; - } - - String? logInTokenID = AppState().postParamsObject?.logInTokenID; - if (logInTokenID == null || logInTokenID.isEmpty) { - logInTokenID = "@dm!n"; - } - - Map postParams = { - "VersionID": 20, - "Channel": 31, - "LanguageID": context != null ? AppState().getLanguageID(context) : 2, - "LogInTokenID": logInTokenID, - "TokenID": tokenID, - "P_USER_NAME": pUserName, - "WP_CategoryID": (categoryId == "all" || categoryId == null) ? "" : categoryId, - "WP_Page": page, - "WP_Per_page": perPage, - }; + postParams["MemberType"] = ApiConsts.isDependetLogin ? 2 : 1; + postParams["CompanyID"] = 1; + postParams["WP_CategoryID"] = effectiveCatId; + postParams["WP_Page"] = page; + postParams["WP_Per_page"] = perPage; if (geoLocation != null && geoLocation.isNotEmpty) { postParams["WP_Geo_Location"] = geoLocation; } + _cleanWalaPlusParams(postParams); return await ApiClient().postJsonForObject( (response) { - if (response != null && response['WalaPlus_Retailers'] != null) { - var val = response['WalaPlus_Retailers']; + if (response != null) { + dynamic val = response['WalaPlus_Retailers'] ?? + response['walaPlus_Retailers'] ?? + response['WalaPlusRetailers'] ?? + response['WP_Retailers'] ?? + response['result']; if (val is String) { - val = json.decode(val); + try { + val = json.decode(val); + } catch (e) { + debugPrint("Error decoding WalaPlus_Retailers: $e"); + } } if (val is Map) { return WalaPlusRetailersResponse.fromJson(val); } else if (val is Map) { return WalaPlusRetailersResponse.fromJson(Map.from(val)); + } else if (val is List) { + List retailers = []; + for (var v in val) { + if (v is Map) { + retailers.add(WalaPlusRetailerModel.fromJson(v)); + } else if (v is Map) { + retailers.add(WalaPlusRetailerModel.fromJson(Map.from(v))); + } + } + return WalaPlusRetailersResponse(retailers: retailers, page: 1, pagesCount: 1, perPage: retailers.length); } } return null; @@ -183,48 +265,161 @@ class OffersAndDiscountsApiClient { Future getWalaPlusRetailerById({ required String retailerId, BuildContext? context, + String? mobileNumber, }) async { String url = ApiConsts.walaPlusRetailersByID; - - String? pUserName = AppState().getUserName; - if (pUserName == null || pUserName.isEmpty) { - pUserName = "2537367522"; - } - - String? tokenID = AppState().postParamsObject?.tokenID; - if (tokenID == null || tokenID.isEmpty) { - tokenID = "adm!n"; - } - - String? logInTokenID = AppState().postParamsObject?.logInTokenID; - if (logInTokenID == null || logInTokenID.isEmpty) { - logInTokenID = "@dm!n"; + String pUserName = _resolveUserName(); + String pMobileNumber = _resolveMobileNumber(overrideMobile: mobileNumber); + String pDepNationalId = _resolveDepNationalId(); + + Map postParams = _getBaseWalaPlusParams(); + postParams["WP_RetailerID"] = retailerId; + postParams["RetailerID"] = retailerId; + postParams["UserName"] = pUserName; + postParams["P_USER_NAME"] = pUserName; + postParams["P_DEP_NATIONAL_ID"] = pDepNationalId; + if (pMobileNumber.isNotEmpty) { + postParams["MobileNumber"] = pMobileNumber; } + postParams["MemberType"] = ApiConsts.isDependetLogin ? 2 : 1; + postParams["CompanyID"] = 1; + _cleanWalaPlusParams(postParams); - Map postParams = { - "VersionID": 20, - "Channel": 31, - "LanguageID": context != null ? AppState().getLanguageID(context) : 2, - "LogInTokenID": logInTokenID, - "TokenID": tokenID, - "RetailerID": retailerId, - "P_USER_NAME": pUserName, - }; + debugPrint("getWalaPlusRetailerById: calling $url with WP_RetailerID: $retailerId"); return await ApiClient().postJsonForObject( (response) { - if (response != null && response['WalaPlus_RetailersByID'] != null) { - var val = response['WalaPlus_RetailersByID']; + if (response != null) { + debugPrint("getWalaPlusRetailerById response keys: ${response is Map ? response.keys.toList() : 'non-map'}"); + dynamic val = response['WalaPlus_RetailersByID'] ?? + response['walaPlus_RetailersByID'] ?? + response['WalaPlusRetailersByID'] ?? + response['WP_RetailersByID'] ?? + response['result']; if (val is String) { - val = json.decode(val); + try { + val = json.decode(val); + } catch (e) { + debugPrint("Error decoding WalaPlus_RetailersByID JSON string: $e"); + } } + WalaPlusRetailerModel? model; if (val is List && val.isNotEmpty) { var item = val.first; if (item is Map) { - return WalaPlusRetailerModel.fromJson(item); + model = WalaPlusRetailerModel.fromJson(item); } else if (item is Map) { - return WalaPlusRetailerModel.fromJson(Map.from(item)); + model = WalaPlusRetailerModel.fromJson(Map.from(item)); + } + } else if (val is Map) { + Map map = (val is Map) ? val : Map.from(val); + if (map.containsKey('retailer')) { + var byIdResponse = WalaPlusRetailersByIDResponse.fromJson(map); + model = byIdResponse.primaryRetailer ?? WalaPlusRetailerModel.fromJson(map); + } else { + model = WalaPlusRetailerModel.fromJson(map); + } + if ((model?.offers == null || model!.offers!.isEmpty) && (map.containsKey('offers') || map.containsKey('Offers'))) { + model?.offers = WalaPlusRetailerModel.parseOffersList(map['offers'] ?? map['Offers']); + } + if ((model?.locations == null || model!.locations!.isEmpty) && + (map.containsKey('locations') || map.containsKey('Locations') || map.containsKey('location') || map.containsKey('Location'))) { + model?.locations = WalaPlusRetailerModel.parseLocationsList( + map['locations'] ?? map['Locations'] ?? map['location'] ?? map['Location'], + ); } + } else if (response is Map && response.containsKey('name')) { + model = WalaPlusRetailerModel.fromJson(response); + } + + if (model != null) { + if (response is Map) { + if ((model.offers == null || model.offers!.isEmpty) && (response.containsKey('offers') || response.containsKey('Offers'))) { + model.offers = WalaPlusRetailerModel.parseOffersList(response['offers'] ?? response['Offers']); + } + if ((model.locations == null || model.locations!.isEmpty) && + (response.containsKey('locations') || response.containsKey('Locations') || response.containsKey('location') || response.containsKey('Location'))) { + model.locations = WalaPlusRetailerModel.parseLocationsList( + response['locations'] ?? response['Locations'] ?? response['location'] ?? response['Location'], + ); + } + } + debugPrint("getWalaPlusRetailerById: parsed retailer '${model.name}', offers: ${model.offers?.length ?? 0}, locations: ${model.locations?.length ?? 0}"); + return model; + } + } + return null; + }, + url, + postParams, + ); + } + + String _resolveUserReference({String? overrideUserReference}) { + if (overrideUserReference != null && overrideUserReference.trim().isNotEmpty) { + return overrideUserReference.trim(); + } + String? ref = AppState().memberInformationList?.eMPLOYEENUMBER?.toString(); + if (ref != null && ref.trim().isNotEmpty) { + return ref.trim(); + } + String? pUserName = AppState().getUserName; + if (pUserName != null && pUserName.trim().isNotEmpty) { + return pUserName.trim(); + } + return ""; + } + + Future?> redeemWalaPlusOffer({ + required String offerId, + String? offerType, + String? userReference, + BuildContext? context, + }) async { + String url = ApiConsts.walaPlusOfferRedeem; + String pUserName = _resolveUserName(); + String pUserReference = _resolveUserReference(overrideUserReference: userReference); + + Map postParams = _getBaseWalaPlusParams(); + postParams["WP_User_Reference"] = pUserReference; + postParams["WP_Offer_Id"] = offerId; + postParams["WP_Offer_Type"] = (offerType != null && offerType.trim().isNotEmpty) ? offerType.trim() : "promo"; + postParams["UserName"] = pUserName; + postParams["P_USER_NAME"] = pUserName; + + if (ApiConsts.isDependetLogin) { + String pDepNationalId = _resolveDepNationalId(); + if (pDepNationalId.isNotEmpty) { + postParams["P_DEP_NATIONAL_ID"] = pDepNationalId; + } + postParams["MemberType"] = 2; + } else { + postParams["P_DEP_NATIONAL_ID"] = ""; + postParams["MemberType"] = 1; + } + _cleanWalaPlusParams(postParams); + + debugPrint("redeemWalaPlusOffer calling $url with params: $postParams"); + + return await ApiClient().postJsonForObject( + (response) { + if (response != null) { + debugPrint("redeemWalaPlusOffer response: $response"); + dynamic val = response['WalaPlus_OfferRedeem'] ?? + response['walaPlus_OfferRedeem'] ?? + response['WalaPlusOfferRedeem'] ?? + response['result']; + if (val is String) { + try { + val = json.decode(val); + } catch (e) { + debugPrint("Error decoding WalaPlus_OfferRedeem: $e"); + } + } + if (val is Map) { + return val; + } else if (val is Map) { + return Map.from(val); } } return null; diff --git a/lib/api/profile_api_client.dart b/lib/api/profile_api_client.dart index 342ac31..c9d5794 100644 --- a/lib/api/profile_api_client.dart +++ b/lib/api/profile_api_client.dart @@ -24,6 +24,8 @@ import 'package:mohem_flutter_app/models/profile/submit_address_transaction.dart import 'package:mohem_flutter_app/models/profile/submit_basic_details_transaction_model.dart'; import 'package:mohem_flutter_app/models/profile/submit_contact_transaction_list_model.dart'; import 'package:mohem_flutter_app/models/profile/submit_phone_transactions.dart'; +import 'package:mohem_flutter_app/models/profile/patient_info_model.dart'; +import 'package:mohem_flutter_app/classes/utils.dart'; import 'package:mohem_flutter_app/models/start_phone_approval_process_model.dart'; class ProfileApiClient { @@ -417,4 +419,62 @@ class ProfileApiClient { return responseData.getPerformanceAppraisalList ?? []; }, url, postParams); } + + Future getPatientInfo({ + required String nationalId, + required String dob, + String? logInTokenID, + }) async { + // NHIC Service GetPatientInfo only works on Production (hmgwebservices.com) + // String url = "https://hmgwebservices.com/Services/NHIC.svc/REST/GetPatientInfo"; + String url = "${ApiConsts.erpRest}GetMemberInfo"; + String token = logInTokenID ?? + AppState().postParamsObject?.logInTokenID ?? + ""; + + Map postParams = { + "PatientIdentificationID": int.tryParse(nationalId.trim()) ?? nationalId.trim(), + "PatientOutSA": 0, + "DOB": dob.trim(), + "LogInTokenID": token, + "PatientMobileNumber":AppState().postParamsObject?.mobileNumber, // added this as per e-services changed + "TokenID":AppState().postParamsObject?.tokenID,// added this as per e-services changed + "LanguageID":AppState().postParamsObject?.languageID,// added this as per e-services changed + "Channel":AppState().postParamsObject?.channel, + "VersionID": AppState().postParamsObject?.versionID, + "UserName":AppState().postParamsObject?.userName + }; + print(jsonEncode(postParams)); + return await ApiClient().postJsonForObject((json) { + return PatientInfoModel.fromJson(json); + }, url, postParams); + } + + Future processContactMazayaAccess({ + required String accessAction, + int? contactRelationshipId, + int? contactPersonId, + int? personId, + }) async { + String url = "${ApiConsts.erpRest}PROCESS_CONTACT_MAZAYA_ACCESS"; + String action = accessAction.trim().toLowerCase() == "revoke" ? "Revoke" : "Grant"; + Map postParams = { + "P_MENU_TYPE": "E", + "P_SELECTED_RESP_ID": -999, + "P_ACCESS_ACTION": action, + }; + if (contactRelationshipId != null) { + postParams["P_CONTACT_RELATIONSHIP_ID"] = contactRelationshipId; + } + if (contactPersonId != null) { + postParams["P_CONTACT_PERSON_ID"] = contactPersonId; + } + if (personId != null) { + postParams["P_PERSON_ID"] = personId; + } + postParams.addAll(AppState().postParamsJson); + return await ApiClient().postJsonForObject((json) { + return GenericResponseModel.fromJson(json); + }, url, postParams); + } } diff --git a/lib/app_state/app_state.dart b/lib/app_state/app_state.dart index e518ea5..91f8be6 100644 --- a/lib/app_state/app_state.dart +++ b/lib/app_state/app_state.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:easy_localization/easy_localization.dart'; import 'package:mohem_flutter_app/models/chat/get_user_login_token_model.dart'; import 'package:mohem_flutter_app/models/itg_forms_models/request_detail_model.dart'; +import 'package:mohem_flutter_app/models/generic_response_model.dart'; import 'package:mohem_flutter_app/models/member_information_list_model.dart'; import 'package:mohem_flutter_app/models/member_login_list_model.dart'; import 'package:mohem_flutter_app/models/my_team/get_employee_subordinates_list.dart'; @@ -92,11 +93,58 @@ class AppState { final PostParamsModel _postParamsInitConfig = PostParamsModel(channel: 31, versionID: 10.1, mobileType: Platform.isAndroid ? "android" : "ios"); + PostParamsModel get postParamsInitConfig { + if (_postParamsInitConfig.pLegislationCode == null) { + if (_postParams?.pLegislationCode != null) { + _postParamsInitConfig.pLegislationCode = _postParams?.pLegislationCode; + } else if (_memberLoginList?.pLEGISLATIONCODE != null) { + _postParamsInitConfig.pLegislationCode = _memberLoginList?.pLEGISLATIONCODE; + } + } + return _postParamsInitConfig; + } + + GetWalaplusDepInfoModel? _walaplusDepInfoModel; + + GetWalaplusDepInfoModel? get walaplusDepInfoModel => _walaplusDepInfoModel; + + set setWalaplusDepInfoModel(GetWalaplusDepInfoModel? model) => _walaplusDepInfoModel = model; + + String? _dependentName; + + String? get dependentName => _dependentName ?? _walaplusDepInfoModel?.pDependentName; + + set setDependentName(String? name) => _dependentName = name; + + int? _dependentLoginType; + + int? get dependentLoginType => _dependentLoginType; + + set setDependentLoginType(int? type) => _dependentLoginType = type; + + String? _dependentDob; + + String? get dependentDob => _dependentDob; + + set setDependentDob(String? dob) => _dependentDob = dob; + + String? _authenticationTokenID; + + String? get authenticationTokenID => _authenticationTokenID; + + set setAuthenticationTokenID(String? token) => _authenticationTokenID = token; + void setPostParamsInitConfig() { isAuthenticated = false; isLogged = false; _memberLoginList = null; _memberInformationList = null; + _walaplusDepInfoModel = null; + _dependentName = null; + _dependentLoginType = null; + _dependentDob = null; + _authenticationTokenID = null; + _postParamsInitConfig.pLegislationCode = null; _postParams = _postParamsInitConfig; } @@ -143,7 +191,13 @@ class AppState { MemberLoginListModel? get memberLoginList => _memberLoginList; - set setMemberLoginListModel(MemberLoginListModel? _memberLoginList) => this._memberLoginList = _memberLoginList; + set setMemberLoginListModel(MemberLoginListModel? _memberLoginList) { + this._memberLoginList = _memberLoginList; + if (_memberLoginList?.pLEGISLATIONCODE != null) { + _postParamsInitConfig.pLegislationCode = _memberLoginList?.pLEGISLATIONCODE?.trim().toUpperCase(); + _postParams?.pLegislationCode = _memberLoginList?.pLEGISLATIONCODE?.trim().toUpperCase(); + } + } MemberInformationListModel? _memberInformationList; diff --git a/lib/classes/consts.dart b/lib/classes/consts.dart index 74d1572..dfe42c4 100644 --- a/lib/classes/consts.dart +++ b/lib/classes/consts.dart @@ -1,7 +1,7 @@ import 'package:mohem_flutter_app/ui/marathon/widgets/question_card.dart'; class ApiConsts { - // static String baseUrl = "http://10.200.204.11"; // Local server + // static String baseUrl = "http://10.201.214.101:2027"; // Local server // static String baseUrl = "https://erptstapp.srca.org.sa"; // SRCA server // static String baseUrl = "https://uat.hmgwebservices.com"; // UAT ser343622ver @@ -9,10 +9,10 @@ class ApiConsts { // static String baseUrl = "https://webservices.hmg.com"; // PreProd // static String baseUrl = "https://hmgwebservices.com"; // Live server - // static String baseUrl = "https://mohemm.hmg.com"; // New Live server + static String baseUrl = "https://mohemm.hmg.com"; // New Live server static String ssoBaseUrl = "https://sso.hmg.com"; // New Live server // - static String baseUrl = "https://uat.hmgwebservices.com"; // UAT ser343622ver + // static String baseUrl = "https://uat.hmgwebservices.com"; // UAT ser343622ver // static String baseUrl = "http://10.20.200.111:1010/"; // static String baseUrl = "https://mohemm.hmg.com"; // New Live server //static String eLearningBaseUrl = "https://elearning.hmg.com/moodle_dev/api/"; // Elearning server UAT @@ -43,6 +43,8 @@ class ApiConsts { static String walaPlusCategories = erpRest + "WalaPlus_Categories"; static String walaPlusRetailers = erpRest + "WalaPlus_Retailers"; static String walaPlusRetailersByID = erpRest + "WalaPlus_RetailersByID"; + static String walaPlusOfferRedeem = erpRest + "WalaPlus_OfferRedeem"; + static String processContactMazayaAccess = erpRest + "PROCESS_CONTACT_MAZAYA_ACCESS"; static String ssoAuthRedirection = ssoBaseUrl + "/api/auth/connect"; diff --git a/lib/classes/inAppWebView.dart b/lib/classes/inAppWebView.dart index ccb24fd..d19f951 100644 --- a/lib/classes/inAppWebView.dart +++ b/lib/classes/inAppWebView.dart @@ -1,6 +1,7 @@ import 'dart:developer'; import 'package:flutter_inappwebview/flutter_inappwebview.dart'; +import 'package:permission_handler/permission_handler.dart'; class MyInAppBrowser extends InAppBrowser { final Function onExitCallback; @@ -30,4 +31,21 @@ class MyInAppBrowser extends InAppBrowser { void onLoadError(Uri? url, int code, String message) { log("Can't load $url.. Error: $message"); } + + @override + Future onPermissionRequest(PermissionRequest permissionRequest) async { + log("onPermissionRequest: ${permissionRequest.resources}"); + try { + var status = await Permission.camera.status; + if (!status.isGranted) { + await Permission.camera.request(); + } + } catch (e) { + log("Error checking/requesting camera permission: $e"); + } + return PermissionResponse( + resources: permissionRequest.resources, + action: PermissionResponseAction.GRANT, + ); + } } diff --git a/lib/classes/utils.dart b/lib/classes/utils.dart index 2ae4fe3..9971317 100644 --- a/lib/classes/utils.dart +++ b/lib/classes/utils.dart @@ -393,7 +393,11 @@ class Utils { AppState().isLogged = false; AppState().setPostParamsInitConfig(); if (chatData != null) { - chatData.disposeData(); + try { + chatData.disposeData(); + } catch (e) { + + } } // SharedPreferences prefs = await SharedPreferences.getInstance(); // await prefs.clear(); diff --git a/lib/config/routes.dart b/lib/config/routes.dart index a9d868f..e555d02 100644 --- a/lib/config/routes.dart +++ b/lib/config/routes.dart @@ -48,6 +48,8 @@ import 'package:mohem_flutter_app/ui/my_team/view_attendance.dart'; import 'package:mohem_flutter_app/ui/parking_qr/parking_qr.dart'; import 'package:mohem_flutter_app/ui/payslip/monthly_pay_slip_screen.dart'; import 'package:mohem_flutter_app/ui/performance/performance_appraisal_screen.dart'; +import 'package:mohem_flutter_app/ui/profile/add_dependent_verify_screen.dart'; +import 'package:mohem_flutter_app/ui/profile/mazaya_access_result_screen.dart'; import 'package:mohem_flutter_app/ui/profile/add_update_family_member.dart'; import 'package:mohem_flutter_app/ui/profile/basic_details.dart'; import 'package:mohem_flutter_app/ui/profile/contact_details.dart'; @@ -85,6 +87,7 @@ import 'package:mohem_flutter_app/ui/work_list/itg_detail_screen.dart'; import 'package:mohem_flutter_app/ui/work_list/work_list_screen.dart'; import 'package:mohem_flutter_app/ui/work_list/worklist_detail_screen.dart'; import 'package:mohem_flutter_app/ui/work_list/worklist_settings.dart'; +import 'package:mohem_flutter_app/widgets/app_in_app_webview_screen.dart'; class AppRoutes { static GlobalKey navigatorKey = GlobalKey(); @@ -146,6 +149,8 @@ class AppRoutes { static const String familyMembers = "/familyMembers"; static const String deleteFamilyMember = "/deleteFamilyMember"; static const String addUpdateFamilyMember = "/addUpdateFamilyMember"; + static const String addDependentVerify = "/addDependentVerify"; + static const String mazayaAccessResult = "/mazayaAccessResult"; // Mowadhafhi static const String mowadhafhi = "/mowadhafhi"; @@ -229,6 +234,7 @@ class AppRoutes { //Parking QR static const String parkingQr = "/parkingQrScreen"; + static const String appInAppWebView = "/appInAppWebView"; static final Map routes = { login: (BuildContext context) => LoginScreen(), @@ -237,6 +243,16 @@ class AppRoutes { dashboard: (BuildContext context) => DashboardScreen(), dashboardScreenForDependet: (BuildContext context) => const DashboardScreenForDependet(), walaPlusRetailerDetails: (BuildContext context) => const WalaPlusRetailerDetailsScreen(), + appInAppWebView: (BuildContext context) { + Object? args = ModalRoute.of(context)?.settings.arguments; + if (args is Map) { + return AppInAppWebViewScreen( + url: args['url'] ?? "", + title: args['title'], + ); + } + return const Scaffold(); + }, survey: (BuildContext context) => SurveyScreen(), advertisement: (BuildContext context) => ITGAdsScreen(), @@ -287,6 +303,8 @@ class AppRoutes { deleteFamilyMember: (BuildContext context) => DeleteFamilyMember(ModalRoute.of(context)!.settings.arguments as int), requestSubmitScreen: (BuildContext context) => RequestSubmitScreen(), addUpdateFamilyMember: (BuildContext context) => AddUpdateFamilyMember(), + addDependentVerify: (BuildContext context) => const AddDependentVerifyScreen(), + mazayaAccessResult: (BuildContext context) => const MazayaAccessResultScreen(), //mowadhafhi mowadhafhi: (BuildContext context) => MowadhafhiHome(), @@ -358,7 +376,6 @@ class AppRoutes { etqanOvr: (BuildContext context) => EtqanOvrHome(), etqanCreateRequest: (BuildContext context) => EtqanOvrCreateRequest(), etqanGetIncidientRequest: (BuildContext context) => EtqanOvrRequestDetailed(), - activityScreen:(BuildContext context) => const EventActivityScreen(), //Parking QR parkingQr: (BuildContext context) => const ParkingQrScreen(), diff --git a/lib/extensions/string_extensions.dart b/lib/extensions/string_extensions.dart index 93a6cf8..a3dddff 100644 --- a/lib/extensions/string_extensions.dart +++ b/lib/extensions/string_extensions.dart @@ -134,14 +134,16 @@ extension EmailValidator on String { ), ); - Widget toText13({Color? color, bool isUnderLine = false}) => Text( + Widget toText13({Color? color, bool isUnderLine = false, bool isBold = false, int? maxlines, TextOverflow? overflow}) => Text( this, - style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: color ?? MyColors.darkTextColor, letterSpacing: -0.52, decoration: isUnderLine ? TextDecoration.underline : null), + maxLines: maxlines, + overflow: overflow, + style: TextStyle(fontSize: 13, fontWeight: isBold ? FontWeight.bold : FontWeight.w600, color: color ?? MyColors.darkTextColor, letterSpacing: -0.52, decoration: isUnderLine ? TextDecoration.underline : null), ); Widget toText14({Color? color, bool isUnderLine = false, bool isBold = false, FontWeight? weight, int? maxlines, TextAlign? textAlign, bool isCenter = false}) => Text( this, - textAlign: isCenter ? TextAlign.center : (textAlign ?? TextAlign.left), + textAlign: isCenter ? TextAlign.center : textAlign, maxLines: maxlines, style: TextStyle( color: color ?? MyColors.darkTextColor, diff --git a/lib/generated/codegen_loader.g.dart b/lib/generated/codegen_loader.g.dart index 516fe03..242fde5 100644 --- a/lib/generated/codegen_loader.g.dart +++ b/lib/generated/codegen_loader.g.dart @@ -336,6 +336,8 @@ class CodegenLoader extends AssetLoader{ "createdFor": "انشاء لأجل", "requestCreatedSuccessfully": "تم انشاء الطلب بنجاح", "search": "بحث", + "filterEnterPrDetailsHere": "ادخل تفاصيل طلب الشراء هنا", + "filterEnterPoDetailsHere": "ادخل تفاصيل أمر الشراء هنا", "wantToReject": "هل انت متأكد تريد الرفض", "requestType": "نوع الطلب", "employeeDigitalID": "هويةالموظف الرقمية", @@ -395,7 +397,7 @@ class CodegenLoader extends AssetLoader{ "browseCategories": "تصفح الفئات", "searchItems": "عناصر البحث", "offerAndDiscounts": "العروض والخصومات", - "offerValid": "العرض صالح", + "offerValid": "العرض ساري", "offerExpired": "انتهى العرض", "whatAreYouOffering": "ما الذي تعرضه؟", "selectCategory": "اختر الفئة", @@ -690,7 +692,109 @@ class CodegenLoader extends AssetLoader{ "costCenter": "مركز التكلفة", "priceSAR": "السعر (ريال سعودي)", "amountSAR": "المبلغ (ريال سعودي)", - "amuLastThreeMonths": "AMU (آخر 3 أشهر)" + "amuLastThreeMonths": "AMU (آخر 3 أشهر)", + "mazayaTitle": "مزايا", + "mazayaAccessGranted": "تم منح حق الوصول لمزايا", + "mazayaAccessGrantedDesc": "أصبح لدى {} حق وصول نشط الآن. شارك رابط التنزيل أدناه ليتمكن من تثبيت تطبيق مهم وإكمال الإعداد.", + "accessIsNowActive": "الوصول نشط الآن", + "accessIsNowActiveDesc": "يمكن لـ {} استخدام الرابط المرفق لتنزيل تطبيق مزايا ومتابعة الإعداد على جهازه.", + "dependentAccessActive": "وصول التابع نشط", + "shareableAppDownloadLink": "رابط تنزيل التطبيق للمشاركة", + "shareableAppDownloadLinkDesc": "أرسل هذا الرابط إلى {} ليتمكن من تنزيل تطبيق مزايا ومتابعة الإعداد.", + "downloadLink": "رابط التنزيل", + "shareDownloadLink": "مشاركة رابط التنزيل", + "copyLink": "نسخ الرابط", + "done": "تم", + "linkCopied": "تم نسخ الرابط بنجاح", + "mazayaAccessRevoked": "تم إلغاء الوصول لمزايا", + "mazayaAccessRevokedDesc": "قام الكفيل بإلغاء وصول التابع. لم يعد بالإمكان فتح العروض أو تسجيل الدخول بالبصمة.", + "contactYourSponsor": "تواصل مع الكفيل", + "contactYourSponsorDesc": "اطلب من كفيلك السماح بالوصول لمزايا مرة أخرى من تفاصيل التابعين. يمكن للكفيل فقط استعادة الوصول.", + "dependentAccessBlocked": "تم حظر وصول التابع", + "returnToSignIn": "العودة إلى تسجيل الدخول", + "allowMazayaAccess": "السماح بالوصول لمزايا", + "revokeMazayaAccess": "إلغاء الوصول لمزايا", + "active": "نشط", + "noAccess": "غير متاح", + "offerDetails": "تفاصيل العرض", + "offerIdNotAvailable": "معرف العرض غير متوفر", + "unableToRedeemOffer": "تعذر الحصول على رابط العرض، يرجى المحاولة لاحقاً", + "errorRedeemingOffer": "حدث خطأ أثناء استخدام العرض", + "useOffer": "استخدام العرض", + "online": "عبر الإنترنت", + "inStore": "في المتجر", + "ksa": "المملكة", + "riyadh": "الرياض", + "noInstructionsAvailable": "لا توجد طريقة استخدام محددة.", + "contactInformation": "معلومات التواصل:", + "humanResources": "الموارد البشرية", + "drSulaimanAlHabib": "سليمان الحبيب", + "allCategories": "الكل", + "noOffersAvailable": "لا توجد عروض متاحة", + "addDependents": "إضافة تابعين", + "myDependents": "التابعون", + "offDiscount": "خصم {}", + "usageLabel": "الاستخدام: ", + "areYouSureWantToLogOut": "هل أنت متأكد من تسجيل الخروج؟", + "searchDependents": "البحث في التابعين", + "searchFamilyMembers": "البحث في أفراد العائلة", + "addAnotherUser": "إضافة مستخدم آخر", + "joinMazaya": "انضم إلى مزايا", + "joinMazayaSubtitle": "احصل على أفضل العروض والصفقات الحصرية في المطاعم والتسوق ومكافآت نمط الحياة.", + "continueAsEmployee": "المتابعة كموظف", + "continueAsEmployeeSubtitle": "سجل الدخول باستخدام اسم المستخدم وكلمة المرور الخاصة بالموظف.", + "pleaseEnterNationalId": "الرجاء إدخال رقم الهوية الوطنية", + "pleaseSelectDob": "الرجاء تحديد تاريخ الميلاد", + "unableToRetrieveDependentInfo": "تعذر الحصول على معلومات التابع", + "failedToSendOtp": "فشل إرسال رمز التحقق", + "verificationCodeResent": "تم إعادة إرسال رمز التحقق", + "nationalId": "الهوية الوطنية", + "addDependent": "إضافة تابع", + "stepVerify": "التحقق", + "stepBasic": "الأساسية", + "stepAdditional": "إضافية", + "findYourDependent": "البحث عن تابعك", + "findYourDependentDesc": "أدخل التفاصيل تماماً كما تظهر في الهوية الوطنية. سنقوم باسترداد السجل بأمان من مركز المعلومات الوطني (NHIC).", + "enter10DigitNationalId": "أدخل رقم الهوية المكون من 10 أرقام", + "nationalIdMustBe10Digits": "يجب أن يتكون رقم الهوية من 10 أرقام", + "dateFormatPlaceholder": "يوم / شهر / سنة", + "searchUser": "بحث عن المستخدم", + "dependentNotFoundNhic": "لم يتم العثور على سجل لهذا التابع في مركز المعلومات الوطني", + "basicInfo": "المعلومات الأساسية", + "basicInfoDesc": "بيانات الهوية المطلوبة المستردة من مركز المعلومات الوطني.", + "additionalInfo": "معلومات إضافية", + "additionalInfoDesc": "إضافة أو مراجعة بيانات الكفالة والأهلية.", + "select": "اختر", + "enterFieldPlaceholder": "أدخل {}", + "requiredField": "الحقل المطلوب", + "pleaseFillField": "الرجاء إدخال {}", + "reviewAndSubmit": "المراجعة والإرسال", + "reviewAndSubmitDesc": "يرجى مراجعة تفاصيل التابع بعناية قبل تأكيد الإرسال.", + "arabicName": "الاسم بالعربية", + "relationship": "صلة القرابة", + "sponsor": "الكفيل", + "dependentSponsorDetail": "تفاصيل كفيل التابع", + "insuranceEligibility": "أهلية التأمين", + "relationshipStartDate": "تاريخ بدء العلاقة", + "dependentAddedSuccessfully": "تم إرسال الطلب بنجاح", + "genderMale": "ذكر", + "genderFemale": "أنثى", + "religionIslam": "الإسلام", + "back": "السابق", + "submitRequest": "إرسال الطلب", + "enterDependentDetailsToContinue": "أدخل تفاصيل التابع للمتابعة.", + "enterBasicDetailsDesc": "يرجى إدخال البيانات الأساسية للتابع.", + "locations": "الفروع والمواقع", + "availableOffers": "العروض المتاحة", + "selectOfferToRedeem": "اختر العرض الذي ترغب في استخدامه", + "viewOnMap": "عرض على الخريطة", + "showAllLocations": "عرض جميع الفروع ({})", + "showLess": "عرض أقل", + "searchBranches": "البحث عن الفروع أو المدن...", + "allCities": "الكل", + "noBranchesFound": "لم يتم العثور على فروع مطابقة", + "browseAllBranches": "تصفح جميع الفروع ({}) في مختلف المدن", + "branchesCount": "{} فرع" }; static const Map _en_US = { "mohemm": "Mohemm", @@ -1009,6 +1113,8 @@ static const Map _en_US = { "requestType": "Request Type", "requestCreatedSuccessfully": "Request created successfully", "search": "Search", + "filterEnterPrDetailsHere": "Enter PR Details here", + "filterEnterPoDetailsHere": "Enter PO Details here", "wantToReject": "Are you sure want to reject?", "employeeDigitalID": "Employee Digital ID", "businessCard": "Business Card", @@ -1368,7 +1474,109 @@ static const Map _en_US = { "costCenter": "Cost Center", "priceSAR": "Price (SAR)", "amountSAR": "Amount (SAR)", - "amuLastThreeMonths": "AMU (Last 3 months)" + "amuLastThreeMonths": "AMU (Last 3 months)", + "mazayaTitle": "Mazaya", + "mazayaAccessGranted": "Mazaya access granted", + "mazayaAccessGrantedDesc": "{} now has active access. Share the download link below so they can install the Mohem app and complete setup.", + "accessIsNowActive": "Access is now active", + "accessIsNowActiveDesc": "{} can use the provided link to download the Mazaya app and continue setup on their device.", + "dependentAccessActive": "Dependent access active", + "shareableAppDownloadLink": "Shareable app-download link", + "shareableAppDownloadLinkDesc": "Send this link to {} so they can download the Mazaya app and continue setup.", + "downloadLink": "Download link", + "shareDownloadLink": "Share Download Link", + "copyLink": "Copy Link", + "done": "Done", + "linkCopied": "Link copied to clipboard", + "mazayaAccessRevoked": "Mazaya access revoked", + "mazayaAccessRevokedDesc": "Your sponsor has revoked your dependent access. You can no longer open offers or sign in with biometrics.", + "contactYourSponsor": "Contact your sponsor", + "contactYourSponsorDesc": "Ask your sponsor to allow Mazaya access again from Dependent Details. Only the sponsor can restore access.", + "dependentAccessBlocked": "Dependent access blocked", + "returnToSignIn": "Return to Sign In", + "allowMazayaAccess": "Allow Mazaya Access", + "revokeMazayaAccess": "Revoke Mazaya Access", + "active": "Active", + "noAccess": "No access", + "offerDetails": "Offer Details", + "offerIdNotAvailable": "Offer ID not available", + "unableToRedeemOffer": "Unable to redeem offer, please try again later", + "errorRedeemingOffer": "An error occurred while redeeming offer", + "useOffer": "Use Offer", + "online": "Online", + "inStore": "In Store", + "ksa": "KSA", + "riyadh": "Riyadh", + "noInstructionsAvailable": "No instructions available.", + "contactInformation": "Contact Information:", + "humanResources": "Human Resources", + "drSulaimanAlHabib": "DR SULAIMAN AL HABIB", + "allCategories": "All", + "noOffersAvailable": "No offers available", + "addDependents": "Add Dependents", + "myDependents": "My Dependents", + "offDiscount": "{} Off", + "usageLabel": "Usage: ", + "areYouSureWantToLogOut": "Are you sure you want to log out?", + "searchDependents": "Search dependents", + "searchFamilyMembers": "Search family members", + "addAnotherUser": "Add another user", + "joinMazaya": "Join Mazaya", + "joinMazayaSubtitle": "Get the best offers and exclusive deals across dining, shopping, and lifestyle rewards.", + "continueAsEmployee": "Continue as an employee", + "continueAsEmployeeSubtitle": "Sign in with your employee username and password.", + "pleaseEnterNationalId": "Please enter National ID", + "pleaseSelectDob": "Please select Date of Birth", + "unableToRetrieveDependentInfo": "Unable to retrieve dependent information", + "failedToSendOtp": "Failed to send verification code", + "verificationCodeResent": "Verification code resent", + "nationalId": "National ID", + "addDependent": "Add Dependent", + "stepVerify": "Verify", + "stepBasic": "Basic", + "stepAdditional": "Additional", + "findYourDependent": "Find your dependent", + "findYourDependentDesc": "Enter the details exactly as they appear on the National ID. We'll securely retrieve the record from NHIC.", + "enter10DigitNationalId": "Enter 10-digit National ID", + "nationalIdMustBe10Digits": "National ID must be 10 digits", + "dateFormatPlaceholder": "DD / MM / YYYY", + "searchUser": "Search User", + "dependentNotFoundNhic": "Sorry, error while getting dependent info from NHIC", + "basicInfo": "Basic Info", + "basicInfoDesc": "Required identity details fetched from NHIC.", + "additionalInfo": "Additional Info", + "additionalInfoDesc": "Add or review sponsorship and eligibility details.", + "select": "Select", + "enterFieldPlaceholder": "Enter {}", + "requiredField": "Required field", + "pleaseFillField": "Please fill {}", + "reviewAndSubmit": "Review & Submit", + "reviewAndSubmitDesc": "Please review the dependent details carefully before submitting.", + "arabicName": "Arabic Name", + "relationship": "Relationship", + "sponsor": "Sponsor", + "dependentSponsorDetail": "Dependent Sponsor Detail", + "insuranceEligibility": "Insurance Eligibility", + "relationshipStartDate": "Start Date", + "dependentAddedSuccessfully": "Dependent added successfully", + "genderMale": "Male", + "genderFemale": "Female", + "religionIslam": "Islam", + "back": "Back", + "submitRequest": "Submit", + "enterDependentDetailsToContinue": "Enter the dependent details to continue.", + "enterBasicDetailsDesc": "Please enter the dependent's basic identity details.", + "locations": "Locations", + "availableOffers": "Available Offers", + "selectOfferToRedeem": "Select an offer to use", + "viewOnMap": "View on map", + "showAllLocations": "Show all {} locations", + "showLess": "Show less", + "searchBranches": "Search branches or cities...", + "allCities": "All", + "noBranchesFound": "No branches found", + "browseAllBranches": "Browse all {} branches across cities", + "branchesCount": "{} Branches" }; static const Map> mapLocales = {"ar_SA": _ar_SA, "en_US": _en_US}; } diff --git a/lib/generated/locale_keys.g.dart b/lib/generated/locale_keys.g.dart index 9abe224..73e9b86 100644 --- a/lib/generated/locale_keys.g.dart +++ b/lib/generated/locale_keys.g.dart @@ -665,5 +665,108 @@ abstract class LocaleKeys { static const priceSAR = 'priceSAR'; static const amountSAR = 'amountSAR'; static const amuLastThreeMonths = 'amuLastThreeMonths'; + static const mazayaTitle = 'mazayaTitle'; + static const mazayaAccessGranted = 'mazayaAccessGranted'; + static const mazayaAccessGrantedDesc = 'mazayaAccessGrantedDesc'; + static const accessIsNowActive = 'accessIsNowActive'; + static const accessIsNowActiveDesc = 'accessIsNowActiveDesc'; + static const dependentAccessActive = 'dependentAccessActive'; + static const shareableAppDownloadLink = 'shareableAppDownloadLink'; + static const shareableAppDownloadLinkDesc = 'shareableAppDownloadLinkDesc'; + static const downloadLink = 'downloadLink'; + static const shareDownloadLink = 'shareDownloadLink'; + static const copyLink = 'copyLink'; + static const done = 'done'; + static const linkCopied = 'linkCopied'; + static const mazayaAccessRevoked = 'mazayaAccessRevoked'; + static const mazayaAccessRevokedDesc = 'mazayaAccessRevokedDesc'; + static const contactYourSponsor = 'contactYourSponsor'; + static const contactYourSponsorDesc = 'contactYourSponsorDesc'; + static const dependentAccessBlocked = 'dependentAccessBlocked'; + static const returnToSignIn = 'returnToSignIn'; + static const allowMazayaAccess = 'allowMazayaAccess'; + static const revokeMazayaAccess = 'revokeMazayaAccess'; + static const active = 'active'; + static const noAccess = 'noAccess'; + static const offerDetails = 'offerDetails'; + static const offerIdNotAvailable = 'offerIdNotAvailable'; + static const unableToRedeemOffer = 'unableToRedeemOffer'; + static const errorRedeemingOffer = 'errorRedeemingOffer'; + static const useOffer = 'useOffer'; + static const online = 'online'; + static const inStore = 'inStore'; + static const ksa = 'ksa'; + static const riyadh = 'riyadh'; + static const noInstructionsAvailable = 'noInstructionsAvailable'; + static const contactInformation = 'contactInformation'; + static const humanResources = 'humanResources'; + static const drSulaimanAlHabib = 'drSulaimanAlHabib'; + static const allCategories = 'allCategories'; + static const noOffersAvailable = 'noOffersAvailable'; + static const addDependents = 'addDependents'; + static const myDependents = 'myDependents'; + static const offDiscount = 'offDiscount'; + static const usageLabel = 'usageLabel'; + static const areYouSureWantToLogOut = 'areYouSureWantToLogOut'; + static const searchDependents = 'searchDependents'; + static const searchFamilyMembers = 'searchFamilyMembers'; + static const addAnotherUser = 'addAnotherUser'; + static const joinMazaya = 'joinMazaya'; + static const joinMazayaSubtitle = 'joinMazayaSubtitle'; + static const continueAsEmployee = 'continueAsEmployee'; + static const continueAsEmployeeSubtitle = 'continueAsEmployeeSubtitle'; + static const pleaseEnterNationalId = 'pleaseEnterNationalId'; + static const pleaseSelectDob = 'pleaseSelectDob'; + static const unableToRetrieveDependentInfo = 'unableToRetrieveDependentInfo'; + static const failedToSendOtp = 'failedToSendOtp'; + static const verificationCodeResent = 'verificationCodeResent'; + static const nationalId = 'nationalId'; + static const addDependent = 'addDependent'; + static const stepVerify = 'stepVerify'; + static const stepBasic = 'stepBasic'; + static const stepAdditional = 'stepAdditional'; + static const findYourDependent = 'findYourDependent'; + static const findYourDependentDesc = 'findYourDependentDesc'; + static const enter10DigitNationalId = 'enter10DigitNationalId'; + static const nationalIdMustBe10Digits = 'nationalIdMustBe10Digits'; + static const dateFormatPlaceholder = 'dateFormatPlaceholder'; + static const searchUser = 'searchUser'; + static const dependentNotFoundNhic = 'dependentNotFoundNhic'; + static const basicInfo = 'basicInfo'; + static const basicInfoDesc = 'basicInfoDesc'; + static const additionalInfo = 'additionalInfo'; + static const additionalInfoDesc = 'additionalInfoDesc'; + static const select = 'select'; + static const enterFieldPlaceholder = 'enterFieldPlaceholder'; + static const requiredField = 'requiredField'; + static const pleaseFillField = 'pleaseFillField'; + static const reviewAndSubmit = 'reviewAndSubmit'; + static const reviewAndSubmitDesc = 'reviewAndSubmitDesc'; + static const arabicName = 'arabicName'; + static const relationship = 'relationship'; + static const sponsor = 'sponsor'; + static const dependentSponsorDetail = 'dependentSponsorDetail'; + static const insuranceEligibility = 'insuranceEligibility'; + static const relationshipStartDate = 'relationshipStartDate'; + static const dependentAddedSuccessfully = 'dependentAddedSuccessfully'; + static const genderMale = 'genderMale'; + static const genderFemale = 'genderFemale'; + static const religionIslam = 'religionIslam'; + static const back = 'back'; + static const submitRequest = 'submitRequest'; + static const enterDependentDetailsToContinue = 'enterDependentDetailsToContinue'; + static const enterBasicDetailsDesc = 'enterBasicDetailsDesc'; + static const locations = 'locations'; + static const availableOffers = 'availableOffers'; + static const selectOfferToRedeem = 'selectOfferToRedeem'; + static const viewOnMap = 'viewOnMap'; + static const showAllLocations = 'showAllLocations'; + static const showLess = 'showLess'; + static const searchBranches = 'searchBranches'; + static const allCities = 'allCities'; + static const noBranchesFound = 'noBranchesFound'; + static const browseAllBranches = 'browseAllBranches'; + static const branchesCount = 'branchesCount'; } + diff --git a/lib/models/dependent/get_mobile_login_info_dependent_model.dart b/lib/models/dependent/get_mobile_login_info_dependent_model.dart new file mode 100644 index 0000000..6ab4114 --- /dev/null +++ b/lib/models/dependent/get_mobile_login_info_dependent_model.dart @@ -0,0 +1,92 @@ +class GetMobileLoginInfoDependentModel { + int? iD; + String? dependentID; + String? depedentName; + String? dob; + int? channelID; + int? companyID; + String? deviceType; + String? deviceToken; + int? language; + int? gender; + int? loginType; + String? createdOn; + String? editedOn; + bool? businessCardPrivilege; + + GetMobileLoginInfoDependentModel({ + this.iD, + this.dependentID, + this.depedentName, + this.dob, + this.channelID, + this.companyID, + this.deviceType, + this.deviceToken, + this.language, + this.gender, + this.loginType, + this.createdOn, + this.editedOn, + this.businessCardPrivilege, + }); + + // Convenience aliases for existing logic + String? get nationalId => dependentID; + String? get birthDate => dob; + String? get employeeName => depedentName; + String? get dependentName => depedentName; + + GetMobileLoginInfoDependentModel.fromJson(Map json) { + iD = json['ID']; + dependentID = (json['DependentID'] ?? + json['DEPENDENT_ID'] ?? + json['P_DEP_NATIONAL_ID'] ?? + json['NationalID'] ?? + json['NationalId'] ?? + json['EmployeeID'] ?? + json['UserName']) + ?.toString(); + depedentName = (json['DepedentName'] ?? + json['DependentName'] ?? + json['P_DEPENDENT_NAME'] ?? + json['EmployeeName']) + ?.toString(); + dob = (json['DOB'] ?? + json['BirthDate'] ?? + json['DateOfBirth'] ?? + json['P_DEP_BIRTH_DATE'] ?? + json['P_BIRTH_DATE'] ?? + json['Birth_Date']) + ?.toString(); + channelID = json['ChannelID'] != null ? int.tryParse(json['ChannelID'].toString()) : null; + companyID = json['CompanyID'] != null ? int.tryParse(json['CompanyID'].toString()) : null; + deviceType = json['DeviceType']?.toString(); + deviceToken = json['DeviceToken']?.toString(); + language = json['Language'] != null ? int.tryParse(json['Language'].toString()) : null; + gender = json['Gender'] != null ? int.tryParse(json['Gender'].toString()) : null; + loginType = json['LoginType'] != null ? int.tryParse(json['LoginType'].toString()) : null; + createdOn = json['CreatedOn']?.toString(); + editedOn = json['EditedOn']?.toString(); + businessCardPrivilege = json['BusinessCardPrivilege']; + } + + Map toJson() { + Map data = {}; + data['ID'] = iD; + data['DependentID'] = dependentID; + data['DepedentName'] = depedentName; + data['DOB'] = dob; + data['ChannelID'] = channelID; + data['CompanyID'] = companyID; + data['DeviceType'] = deviceType; + data['DeviceToken'] = deviceToken; + data['Language'] = language; + data['Gender'] = gender; + data['LoginType'] = loginType; + data['CreatedOn'] = createdOn; + data['EditedOn'] = editedOn; + data['BusinessCardPrivilege'] = businessCardPrivilege; + return data; + } +} diff --git a/lib/models/generic_response_model.dart b/lib/models/generic_response_model.dart index b6f4ef8..9e2d838 100644 --- a/lib/models/generic_response_model.dart +++ b/lib/models/generic_response_model.dart @@ -688,7 +688,7 @@ class GenericResponseModel { serviceName = json['ServiceName']; time = json['Time']; androidLink = json['AndroidLink']; - authenticationTokenID = json['AuthenticationTokenID']; + authenticationTokenID = json['AuthenticationTokenID']?.toString(); data = json['Data']; dataw = json['Dataw']; dietType = json['DietType']; @@ -2009,10 +2009,23 @@ class TicketBookingResult { class GetWalaplusDepInfoModel { String? pEmpEmailAddress; String? pEmpMobileNumber; + String? pDependentName; - GetWalaplusDepInfoModel({this.pEmpEmailAddress, this.pEmpMobileNumber}); - - factory GetWalaplusDepInfoModel.fromJson(Map json) => GetWalaplusDepInfoModel(pEmpEmailAddress: json["P_EMP_EMAIL_ADDRESS"], pEmpMobileNumber: json["P_EMP_MOBILE_NUMBER"]); + GetWalaplusDepInfoModel({ + this.pEmpEmailAddress, + this.pEmpMobileNumber, + this.pDependentName, + }); - Map toJson() => {"P_EMP_EMAIL_ADDRESS": pEmpEmailAddress, "P_EMP_MOBILE_NUMBER": pEmpMobileNumber}; + factory GetWalaplusDepInfoModel.fromJson(Map json) => GetWalaplusDepInfoModel( + pEmpEmailAddress: json["P_EMP_EMAIL_ADDRESS"], + pEmpMobileNumber: json["P_EMP_MOBILE_NUMBER"], + pDependentName: json["P_DEPENDENT_NAME"], + ); + + Map toJson() => { + "P_EMP_EMAIL_ADDRESS": pEmpEmailAddress, + "P_EMP_MOBILE_NUMBER": pEmpMobileNumber, + "P_DEPENDENT_NAME": pDependentName, + }; } diff --git a/lib/models/get_employee_contacts.model.dart b/lib/models/get_employee_contacts.model.dart index df0a41f..be139fa 100644 --- a/lib/models/get_employee_contacts.model.dart +++ b/lib/models/get_employee_contacts.model.dart @@ -8,6 +8,8 @@ class GetEmployeeContactsList { int? pERSONID; String? pRIMARYCONTACTFLAG; String? rELATIONSHIP; + String? mAZAYAACCESSFLG; + dynamic mazayaAccess; GetEmployeeContactsList( {this.cONTACTNAME, @@ -17,7 +19,9 @@ class GetEmployeeContactsList { this.dATEOFBIRTH, this.pERSONID, this.pRIMARYCONTACTFLAG, - this.rELATIONSHIP}); + this.rELATIONSHIP, + this.mAZAYAACCESSFLG, + this.mazayaAccess}); GetEmployeeContactsList.fromJson(Map json) { cONTACTNAME = json['CONTACT_NAME']; @@ -28,18 +32,22 @@ class GetEmployeeContactsList { pERSONID = json['PERSON_ID']; pRIMARYCONTACTFLAG = json['PRIMARY_CONTACT_FLAG']; rELATIONSHIP = json['RELATIONSHIP']; + mAZAYAACCESSFLG = json['MAZAYA_ACCESS_FLG']?.toString() ?? json['MAZAYA_ACCESS']?.toString(); + mazayaAccess = mAZAYAACCESSFLG; } Map toJson() { - Map data = new Map(); - data['CONTACT_NAME'] = this.cONTACTNAME; - data['CONTACT_PERSON_ID'] = this.cONTACTPERSONID; - data['CONTACT_RELATIONSHIP_ID'] = this.cONTACTRELATIONSHIPID; - data['CONTACT_TYPE'] = this.cONTACTTYPE; - data['DATE_OF_BIRTH'] = this.dATEOFBIRTH; - data['PERSON_ID'] = this.pERSONID; - data['PRIMARY_CONTACT_FLAG'] = this.pRIMARYCONTACTFLAG; - data['RELATIONSHIP'] = this.rELATIONSHIP; + Map data = {}; + data['CONTACT_NAME'] = cONTACTNAME; + data['CONTACT_PERSON_ID'] = cONTACTPERSONID; + data['CONTACT_RELATIONSHIP_ID'] = cONTACTRELATIONSHIPID; + data['CONTACT_TYPE'] = cONTACTTYPE; + data['DATE_OF_BIRTH'] = dATEOFBIRTH; + data['PERSON_ID'] = pERSONID; + data['PRIMARY_CONTACT_FLAG'] = pRIMARYCONTACTFLAG; + data['RELATIONSHIP'] = rELATIONSHIP; + data['MAZAYA_ACCESS_FLG'] = mAZAYAACCESSFLG; + data['MAZAYA_ACCESS'] = mazayaAccess; return data; } } \ No newline at end of file diff --git a/lib/models/get_mobile_login_info_list_model.dart b/lib/models/get_mobile_login_info_list_model.dart index d544ef5..00267a2 100644 --- a/lib/models/get_mobile_login_info_list_model.dart +++ b/lib/models/get_mobile_login_info_list_model.dart @@ -1,6 +1,6 @@ class GetMobileLoginInfoListModel { int? iD; - String? employeeID; + dynamic? employeeID; //do not change this int? channelID; int? companyID; String? deviceType; diff --git a/lib/models/offers_and_discounts/wala_plus_category_model.dart b/lib/models/offers_and_discounts/wala_plus_category_model.dart index 51e835f..aac2752 100644 --- a/lib/models/offers_and_discounts/wala_plus_category_model.dart +++ b/lib/models/offers_and_discounts/wala_plus_category_model.dart @@ -11,14 +11,33 @@ class WalaPlusCategoryModel { this.imageThumb, this.name, this.parentId, - }); + }) { + if ((image == null || image!.isEmpty) && id != null && id!.trim().isNotEmpty && id != "all" && id != "0") { + String cleanId = id!.trim(); + if (cleanId.toLowerCase().endsWith('.png')) { + cleanId = cleanId.substring(0, cleanId.length - 4); + } + image = Uri.encodeFull("https://epharmacy.hmg.com/middleware/images/category/$cleanId.png"); + } + } WalaPlusCategoryModel.fromJson(Map json) { - id = json['id']?.toString(); - image = json['image']?.toString(); - imageThumb = json['image_thumb']?.toString(); - name = json['name']?.toString(); - parentId = json['parent_id']?.toString(); + id = (json['id'] ?? json['Id'] ?? json['ID'])?.toString(); + name = (json['name'] ?? json['Name'])?.toString(); + parentId = (json['parent_id'] ?? json['Parent_Id'] ?? json['parentId'])?.toString(); + imageThumb = (json['image_thumb'] ?? json['Image_Thumb'] ?? json['imageThumb'])?.toString(); + + // Use category image URL from HMG middleware using ID from WalaPlus_Categories API: + // https://epharmacy.hmg.com/middleware/images/category/{id}.png + if (id != null && id!.trim().isNotEmpty && id != "all" && id != "0") { + String cleanId = id!.trim(); + if (cleanId.toLowerCase().endsWith('.png')) { + cleanId = cleanId.substring(0, cleanId.length - 4); + } + image = Uri.encodeFull("https://epharmacy.hmg.com/middleware/images/category/$cleanId.png"); + } else { + image = (json['image'] ?? json['Image'])?.toString(); + } } Map toJson() { @@ -33,4 +52,15 @@ class WalaPlusCategoryModel { bool get isMainCategory => parentId == null || parentId!.isEmpty; bool get isSvg => (image ?? '').toLowerCase().contains('.svg'); + + String get categoryImageUrl { + if (id != null && id!.trim().isNotEmpty && id != "all" && id != "0") { + String cleanId = id!.trim(); + if (cleanId.toLowerCase().endsWith('.png')) { + cleanId = cleanId.substring(0, cleanId.length - 4); + } + return Uri.encodeFull("https://epharmacy.hmg.com/middleware/images/category/$cleanId.png"); + } + return image ?? ''; + } } diff --git a/lib/models/offers_and_discounts/wala_plus_retailer_model.dart b/lib/models/offers_and_discounts/wala_plus_retailer_model.dart index 39137fc..e7f7a9c 100644 --- a/lib/models/offers_and_discounts/wala_plus_retailer_model.dart +++ b/lib/models/offers_and_discounts/wala_plus_retailer_model.dart @@ -1,3 +1,6 @@ +import 'dart:convert'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:mohem_flutter_app/generated/locale_keys.g.dart'; import 'package:mohem_flutter_app/models/offers_and_discounts/wala_plus_category_model.dart'; class WalaPlusRetailersResponse { @@ -15,16 +18,32 @@ class WalaPlusRetailersResponse { factory WalaPlusRetailersResponse.fromJson(Map json) { List retailersList = []; - if (json['retailers'] != null && json['retailers'] is List) { - json['retailers'].forEach((v) { - retailersList.add(WalaPlusRetailerModel.fromJson(v)); + dynamic retailersData = json['retailers'] ?? json['Retailers'] ?? json['data'] ?? json['items']; + if (retailersData is String) { + try { + retailersData = jsonDecode(retailersData); + } catch (_) {} + } + if (retailersData is List) { + retailersData.forEach((v) { + if (v is Map) { + retailersList.add(WalaPlusRetailerModel.fromJson(v)); + } else if (v is Map) { + retailersList.add(WalaPlusRetailerModel.fromJson(Map.from(v))); + } }); } return WalaPlusRetailersResponse( - page: json['page'] is int ? json['page'] : int.tryParse(json['page']?.toString() ?? ''), - pagesCount: json['pages_count'] is int ? json['pages_count'] : int.tryParse(json['pages_count']?.toString() ?? ''), - perPage: json['per_page'] is int ? json['per_page'] : int.tryParse(json['per_page']?.toString() ?? ''), + page: json['page'] is int + ? json['page'] + : int.tryParse(json['page']?.toString() ?? json['WP_Page']?.toString() ?? '1'), + pagesCount: json['pages_count'] is int + ? json['pages_count'] + : int.tryParse(json['pages_count']?.toString() ?? json['pagesCount']?.toString() ?? json['totalPages']?.toString() ?? '1'), + perPage: json['per_page'] is int + ? json['per_page'] + : int.tryParse(json['per_page']?.toString() ?? json['perPage']?.toString() ?? json['WP_Per_page']?.toString() ?? '10'), retailers: retailersList, ); } @@ -41,6 +60,95 @@ class WalaPlusRetailersResponse { } } +class WalaPlusRetailersByIDResponse { + int? page; + int? pagesCount; + int? perPage; + List? locations; + List? offers; + List? retailer; + + WalaPlusRetailersByIDResponse({ + this.page, + this.pagesCount, + this.perPage, + this.locations, + this.offers, + this.retailer, + }); + + factory WalaPlusRetailersByIDResponse.fromJson(Map json) { + List locList = WalaPlusRetailerModel.parseLocationsList( + json['locations'] ?? json['Locations'] ?? json['location'] ?? json['Location'], + ); + List offList = WalaPlusRetailerModel.parseOffersList( + json['offers'] ?? json['Offers'], + ); + + List retList = []; + dynamic retData = json['retailer'] ?? json['Retailer']; + if (retData is List) { + for (var r in retData) { + if (r is Map) { + var model = WalaPlusRetailerModel.fromJson(r); + if (model.locations == null || model.locations!.isEmpty) model.locations = locList; + if (model.offers == null || model.offers!.isEmpty) model.offers = offList; + retList.add(model); + } else if (r is Map) { + var model = WalaPlusRetailerModel.fromJson(Map.from(r)); + if (model.locations == null || model.locations!.isEmpty) model.locations = locList; + if (model.offers == null || model.offers!.isEmpty) model.offers = offList; + retList.add(model); + } + } + } else if (retData is Map) { + var model = WalaPlusRetailerModel.fromJson(Map.from(retData)); + if (model.locations == null || model.locations!.isEmpty) model.locations = locList; + if (model.offers == null || model.offers!.isEmpty) model.offers = offList; + retList.add(model); + } + + return WalaPlusRetailersByIDResponse( + page: json['page'] is int + ? json['page'] + : int.tryParse(json['page']?.toString() ?? json['WP_Page']?.toString() ?? ''), + pagesCount: json['pages_count'] is int + ? json['pages_count'] + : int.tryParse(json['pages_count']?.toString() ?? json['pagesCount']?.toString() ?? ''), + perPage: json['per_page'] is int + ? json['per_page'] + : int.tryParse(json['per_page']?.toString() ?? json['perPage']?.toString() ?? ''), + locations: locList, + offers: offList, + retailer: retList, + ); + } + + WalaPlusRetailerModel? get primaryRetailer { + if (retailer != null && retailer!.isNotEmpty) { + return retailer!.first; + } + return null; + } + + Map toJson() { + Map data = {}; + data['page'] = page; + data['pages_count'] = pagesCount; + data['per_page'] = perPage; + if (locations != null) { + data['locations'] = locations!.map((v) => v.toJson()).toList(); + } + if (offers != null) { + data['offers'] = offers!.map((v) => v.toJson()).toList(); + } + if (retailer != null) { + data['retailer'] = retailer!.map((v) => v.toJson()).toList(); + } + return data; + } +} + class WalaPlusRetailerModel { String? id; String? name; @@ -64,8 +172,17 @@ class WalaPlusRetailerModel { String? howToUse; WalaPlusRetailerContact? contact; dynamic offerWeight; + List? offers; List? locations; List? categories; + String? validity; + String? validTo; + String? validUntil; + String? endDate; + String? expiryDate; + String? duration; + String? email; + String? phone; WalaPlusRetailerModel({ this.id, @@ -90,62 +207,223 @@ class WalaPlusRetailerModel { this.howToUse, this.contact, this.offerWeight, + this.offers, this.locations, this.categories, + this.validity, + this.validTo, + this.validUntil, + this.endDate, + this.expiryDate, + this.duration, + this.email, + this.phone, }); + static List parseOffersList(data) { + List list = []; + if (data == null) return list; + if (data is String) { + try { + data = jsonDecode(data); + } catch (_) {} + } + if (data is List) { + for (var v in data) { + if (v is Map) { + list.add(WalaPlusOfferModel.fromJson(v)); + } else if (v is Map) { + list.add(WalaPlusOfferModel.fromJson(Map.from(v))); + } + } + } + return list; + } + + static List parseLocationsList(data) { + List list = []; + if (data == null) return list; + if (data is String) { + try { + data = jsonDecode(data); + } catch (_) {} + } + if (data is List) { + for (var v in data) { + if (v is Map) { + list.add(WalaPlusRetailerLocation.fromJson(v)); + } else if (v is Map) { + list.add(WalaPlusRetailerLocation.fromJson(Map.from(v))); + } + } + } + return list; + } + factory WalaPlusRetailerModel.fromJson(Map json) { - List locationsList = []; - if (json['locations'] != null && json['locations'] is List) { - json['locations'].forEach((v) { - locationsList.add(WalaPlusRetailerLocation.fromJson(v)); - }); + Map targetJson = json; + dynamic offersSource = json['offers'] ?? json['Offers']; + dynamic locationsSource = json['locations'] ?? json['Locations'] ?? json['location'] ?? json['Location']; + + // If json is the wrapper object from WalaPlus_RetailersByID containing 'retailer' key + if (json['retailer'] != null || json['Retailer'] != null) { + dynamic retailerData = json['retailer'] ?? json['Retailer']; + if (retailerData is List && retailerData.isNotEmpty) { + var first = retailerData.first; + if (first is Map) { + targetJson = first; + } else if (first is Map) { + targetJson = Map.from(first); + } + } else if (retailerData is Map) { + targetJson = retailerData; + } else if (retailerData is Map) { + targetJson = Map.from(retailerData); + } + if (locationsSource == null || (locationsSource is List && locationsSource.isEmpty)) { + locationsSource = targetJson['locations'] ?? targetJson['Locations'] ?? targetJson['location'] ?? targetJson['Location']; + } + if (offersSource == null || (offersSource is List && offersSource.isEmpty)) { + offersSource = targetJson['offers'] ?? targetJson['Offers']; + } } + List offersList = parseOffersList(offersSource ?? targetJson['offers'] ?? targetJson['Offers']); + List locationsList = parseLocationsList( + locationsSource ?? targetJson['locations'] ?? targetJson['Locations'] ?? targetJson['location'] ?? targetJson['Location'], + ); + List categoriesList = []; - if (json['categories'] != null && json['categories'] is List) { - json['categories'].forEach((v) { + if (targetJson['categories'] != null && targetJson['categories'] is List) { + targetJson['categories'].forEach((v) { categoriesList.add(WalaPlusCategoryModel.fromJson(v)); }); } WalaPlusRetailerContact? contactObj; - if (json['contact'] != null) { - if (json['contact'] is Map) { - contactObj = WalaPlusRetailerContact.fromJson(Map.from(json['contact'])); - } else if (json['contact'] is String && json['contact'].toString().trim().isNotEmpty) { - contactObj = WalaPlusRetailerContact(website: json['contact'].toString()); + if (targetJson['contact'] != null) { + if (targetJson['contact'] is Map) { + contactObj = WalaPlusRetailerContact.fromJson(Map.from(targetJson['contact'])); + } else if (targetJson['contact'] is String && targetJson['contact'].toString().trim().isNotEmpty) { + contactObj = WalaPlusRetailerContact(website: targetJson['contact'].toString()); + } + } + + String? foundValidity = targetJson['validity']?.toString() ?? + targetJson['valid_to']?.toString() ?? + targetJson['valid_until']?.toString() ?? + targetJson['end_date']?.toString() ?? + targetJson['expiry_date']?.toString() ?? + targetJson['expire_date']?.toString() ?? + targetJson['duration']?.toString() ?? + targetJson['validity_period']?.toString() ?? + targetJson['valid_duration']?.toString() ?? + targetJson['offer_validity']?.toString(); + + if (foundValidity == null || foundValidity == "null" || foundValidity.trim().isEmpty) { + for (var entry in targetJson.entries) { + String k = entry.key.toLowerCase(); + if ((k.contains('valid') || k.contains('expir') || k.contains('duration')) && + !k.contains('isverificationcodevalid') && + !k.contains('barcode')) { + if (entry.value != null && entry.value.toString().trim().isNotEmpty && entry.value.toString() != "null") { + foundValidity = entry.value.toString().trim(); + break; + } + } } } return WalaPlusRetailerModel( - id: json['id']?.toString(), - name: json['name']?.toString(), - offerTitle: json['offer_title']?.toString(), - offerType: json['offer_type']?.toString(), - description: json['description']?.toString(), - info: json['info']?.toString(), - image: json['image']?.toString(), - imageThumb: json['image_thumb']?.toString(), - logo: json['logo']?.toString(), - logoThumb: json['logo_thumb']?.toString(), - website: json['website']?.toString(), - type: json['type']?.toString(), - usage: json['usage'], - weight: json['weight'], - geoId: json['geo_id'], - pin: json['pin']?.toString(), - barcodeStandard: json['barcode_standard']?.toString(), - arHowToUse: json['ar_how_to_use']?.toString(), - enHowToUse: json['en_how_to_use']?.toString(), - howToUse: json['how_to_use']?.toString(), + id: targetJson['id']?.toString() ?? + targetJson['WP_RetailerID']?.toString() ?? + targetJson['retailer_id']?.toString() ?? + targetJson['RetailerID']?.toString() ?? + targetJson['retailerId']?.toString(), + name: decodeUnicodeString(targetJson['name']), + offerTitle: decodeUnicodeString(targetJson['offer_title']), + offerType: targetJson['offer_type']?.toString(), + description: decodeUnicodeString(targetJson['description']), + info: decodeUnicodeString(targetJson['info']), + image: targetJson['image']?.toString(), + imageThumb: targetJson['image_thumb']?.toString(), + logo: targetJson['logo']?.toString(), + logoThumb: targetJson['logo_thumb']?.toString(), + website: targetJson['website']?.toString(), + type: targetJson['type']?.toString(), + usage: targetJson['usage'] is String ? decodeUnicodeString(targetJson['usage']) : targetJson['usage'], + weight: targetJson['weight'], + geoId: targetJson['geo_id'], + pin: targetJson['pin']?.toString(), + barcodeStandard: targetJson['barcode_standard']?.toString(), + arHowToUse: decodeUnicodeString(targetJson['ar_how_to_use']), + enHowToUse: decodeUnicodeString(targetJson['en_how_to_use']), + howToUse: decodeUnicodeString(targetJson['how_to_use']), contact: contactObj, - offerWeight: json['offer_weight'], + offerWeight: targetJson['offer_weight'], + offers: offersList, locations: locationsList, categories: categoriesList, + validity: foundValidity, + validTo: targetJson['valid_to']?.toString() ?? targetJson['validTo']?.toString(), + validUntil: targetJson['valid_until']?.toString() ?? targetJson['validUntil']?.toString(), + endDate: targetJson['end_date']?.toString() ?? targetJson['endDate']?.toString(), + expiryDate: targetJson['expiry_date']?.toString() ?? targetJson['expiryDate']?.toString(), + duration: targetJson['duration']?.toString(), + email: targetJson['email']?.toString() ?? targetJson['contact_email']?.toString() ?? contactObj?.primaryEmail, + phone: targetJson['phone']?.toString() ?? targetJson['contact_phone']?.toString() ?? targetJson['mobile']?.toString() ?? contactObj?.primaryPhone, ); } + static String? decodeUnicodeString(Object? val) { + if (val == null) return null; + String s = val.toString().trim(); + if (s.isEmpty || s == "null") return null; + + while ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) { + if (s.length >= 2) { + s = s.substring(1, s.length - 1).trim(); + } else { + break; + } + } + + s = s + .replaceAll(r'\/', '/') + .replaceAll(r'\"', '"') + .replaceAll(r"\'", "'"); + + s = s.replaceAllMapped(RegExp(r'\\u([0-9a-fA-F]{4})'), (match) { + try { + var codeUnit = int.parse(match.group(1)!, radix: 16); + return String.fromCharCode(codeUnit); + } catch (_) { + return match.group(0)!; + } + }); + + s = s + .replaceAll(r'\u201c', '“') + .replaceAll(r'\u201d', '”') + .replaceAll(r'\u2019', '’') + .replaceAll(r'\u2018', '‘') + .replaceAll(r'\u00a0', ' ') + .replaceAll(r'\u2022', '•') + .replaceAll(r'\u2013', '–') + .replaceAll(r'\u2014', '—') + .replaceAll(r'\u200f', '') + .replaceAll(r'\u200e', '') + .replaceAll(r'\u061f', '؟') + .replaceAll(r'\u201', '“') + .replaceAll('"', '"') + .replaceAll(''', "'") + .replaceAll(''', "'") + .replaceAll(' ', ' ') + .replaceAll('&', '&'); + + return s; + } + Map toJson() { Map data = {}; data['id'] = id; @@ -170,6 +448,17 @@ class WalaPlusRetailerModel { data['how_to_use'] = howToUse; data['contact'] = contact?.toJson(); data['offer_weight'] = offerWeight; + data['validity'] = validity; + data['valid_to'] = validTo; + data['valid_until'] = validUntil; + data['end_date'] = endDate; + data['expiry_date'] = expiryDate; + data['duration'] = duration; + data['email'] = email; + data['phone'] = phone; + if (offers != null) { + data['offers'] = offers!.map((v) => v.toJson()).toList(); + } if (locations != null) { data['locations'] = locations!.map((v) => v.toJson()).toList(); } @@ -179,76 +468,279 @@ class WalaPlusRetailerModel { return data; } + String? get displayImage { + if (image != null && image!.trim().isNotEmpty && image != "null") return image!.trim(); + if (imageThumb != null && imageThumb!.trim().isNotEmpty && imageThumb != "null") return imageThumb!.trim(); + return null; + } + + String? get displayLogo { + if (logo != null && logo!.trim().isNotEmpty && logo != "null") return logo!.trim(); + if (logoThumb != null && logoThumb!.trim().isNotEmpty && logoThumb != "null") return logoThumb!.trim(); + return null; + } + String? get resolvedWebsite { - if (contact?.website != null && contact!.website!.trim().isNotEmpty) { + if (contact?.website != null && contact!.website!.trim().isNotEmpty && contact!.website != "null") { return contact!.website!.trim(); } - if (website != null && website!.trim().isNotEmpty) { + if (website != null && website!.trim().isNotEmpty && website != "null") { return website!.trim(); } return null; } - String get displayCity { + bool get hasPhysicalLocation { if (locations != null && locations!.isNotEmpty) { for (var loc in locations!) { - if (loc.city != null && loc.city!.trim().isNotEmpty) { - return loc.city!.trim(); + if ((loc.cityNameEn != null && loc.cityNameEn!.trim().isNotEmpty && loc.cityNameEn != "null") || + (loc.cityNameAr != null && loc.cityNameAr!.trim().isNotEmpty && loc.cityNameAr != "null") || + (loc.city != null && loc.city!.trim().isNotEmpty && loc.city != "null")) { + return true; } } } - if (type?.toLowerCase() == 'online') { - return 'Online'; + return false; + } + + String getDisplayCity({bool isAr = false}) { + if (locations != null && locations!.isNotEmpty) { + // 1. First pass: look for specific city name + for (var loc in locations!) { + String? cName = isAr ? loc.cityNameAr : loc.cityNameEn; + if (cName != null && cName.trim().isNotEmpty && cName != "null") { + return cName.trim(); + } + } + // 2. Second pass: fallback to city if not generic country + for (var loc in locations!) { + if (loc.city != null && loc.city!.trim().isNotEmpty && loc.city != "null") { + String c = loc.city!.trim(); + if (c.toLowerCase() != 'saudi arabia' && c != 'المملكة العربية السعودية') { + return c; + } + } + } + // 3. Third pass: any non-empty city + for (var loc in locations!) { + if (loc.city != null && loc.city!.trim().isNotEmpty && loc.city != "null") { + return loc.city!.trim(); + } + } } return ''; } + String get displayCity => getDisplayCity(isAr: false); + String get displayUsage { - if (usage != null && usage.toString().isNotEmpty) { - return 'Usage : $usage'; - } - if (type != null && type!.isNotEmpty) { - if (type!.toLowerCase() == 'online') return 'Online'; - if (type!.toLowerCase() == 'store') return 'In Store'; - return type!; + if (usage != null) { + String u = usage.toString().trim(); + if (int.tryParse(u) != null) { + return 'usage:$u'; + } + if (!u.contains('<') && !u.contains('{') && u.isNotEmpty && u.length <= 12 && u.toLowerCase() != 'online' && u.toLowerCase() != 'store') { + return 'usage:$u'; + } } return ''; } + + String getDisplayValidity({bool isAr = false}) { + if (validity != null && validity!.trim().isNotEmpty && validity != "null") { + return validity!.trim(); + } + if (validTo != null && validTo!.trim().isNotEmpty && validTo != "null") { + return validTo!.trim(); + } + if (validUntil != null && validUntil!.trim().isNotEmpty && validUntil != "null") { + return validUntil!.trim(); + } + if (endDate != null && endDate!.trim().isNotEmpty && endDate != "null") { + return endDate!.trim(); + } + if (expiryDate != null && expiryDate!.trim().isNotEmpty && expiryDate != "null") { + return expiryDate!.trim(); + } + if (duration != null && duration!.trim().isNotEmpty && duration != "null") { + return duration!.trim(); + } + return LocaleKeys.offerValid.tr(); + } + + String? get resolvedEmail { + if (email != null && email!.trim().isNotEmpty && email != "null") { + return email!.trim(); + } + if (contact?.primaryEmail != null && contact!.primaryEmail!.trim().isNotEmpty && contact!.primaryEmail != "null") { + return contact!.primaryEmail!.trim(); + } + if (locations != null && locations!.isNotEmpty) { + for (var loc in locations!) { + if (loc.contact != null && loc.contact!.contains('@')) { + return loc.contact!.trim(); + } + } + } + String combined = "${description ?? ''} ${info ?? ''} ${howToUse ?? ''} ${arHowToUse ?? ''} ${enHowToUse ?? ''}"; + RegExp emailRegex = RegExp(r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}'); + Match? match = emailRegex.firstMatch(combined); + if (match != null) { + return match.group(0); + } + return null; + } + + String? get resolvedPhone { + if (phone != null && phone!.trim().isNotEmpty && phone != "null") { + return phone!.trim(); + } + if (contact?.primaryPhone != null && contact!.primaryPhone!.trim().isNotEmpty && contact!.primaryPhone != "null") { + return contact!.primaryPhone!.trim(); + } + if (locations != null && locations!.isNotEmpty) { + for (var loc in locations!) { + if (loc.contact != null && + loc.contact!.trim().isNotEmpty && + loc.contact != "null" && + !loc.contact!.contains('@')) { + return loc.contact!.trim(); + } + } + } + String combined = "${description ?? ''} ${info ?? ''} ${howToUse ?? ''} ${arHowToUse ?? ''} ${enHowToUse ?? ''}"; + RegExp phoneRegex = RegExp(r'(?:\+?966|0)?(?:5\d{8}|1\d{7}|9200\d{5}|800\d{6,7})'); + Match? match = phoneRegex.firstMatch(combined); + if (match != null) { + return match.group(0); + } + return null; + } + + bool get isOnlineStore => (type?.trim().toLowerCase() == 'online'); + + bool get isInstore => (type?.trim().toLowerCase() == 'instore' || + type?.trim().toLowerCase() == 'in_store' || + type?.trim().toLowerCase() == 'in store' || + type?.trim().toLowerCase() == 'store' || + (type == null && locations != null && locations!.isNotEmpty)); + + String? get normalizedType { + if (type != null && type!.trim().isNotEmpty) { + String t = type!.trim().toLowerCase(); + if (t == 'online') return 'online'; + if (t == 'instore' || t == 'in_store' || t == 'in store' || t == 'store') return 'instore'; + return t; + } + if (locations != null && locations!.isNotEmpty) { + return 'instore'; + } + return null; + } + + String? getDisplayType() { + String? norm = normalizedType; + if (norm == 'online') return LocaleKeys.online.tr(); + if (norm == 'instore') return LocaleKeys.inStore.tr(); + if (type != null && type!.trim().isNotEmpty) { + return type!.trim(); + } + return null; + } } class WalaPlusRetailerContact { + String? email; List? emails; + String? phone; List? phones; String? website; + String? address; + String? instagram; + String? twitter; - WalaPlusRetailerContact({this.emails, this.phones, this.website}); + WalaPlusRetailerContact({ + this.email, + this.emails, + this.phone, + this.phones, + this.website, + this.address, + this.instagram, + this.twitter, + }); factory WalaPlusRetailerContact.fromJson(Map json) { List emailsList = []; if (json['emails'] != null && json['emails'] is List) { json['emails'].forEach((v) => emailsList.add(v.toString())); } + String? singleEmail = json['email']?.toString() ?? json['email_address']?.toString(); + if (singleEmail != null && singleEmail.trim().isNotEmpty && singleEmail != "null") { + if (!emailsList.contains(singleEmail.trim())) { + emailsList.add(singleEmail.trim()); + } + } + List phonesList = []; if (json['phones'] != null && json['phones'] is List) { json['phones'].forEach((v) => phonesList.add(v.toString())); } + String? singlePhone = json['phone']?.toString() ?? json['mobile']?.toString() ?? json['phone_number']?.toString(); + if (singlePhone != null && singlePhone.trim().isNotEmpty && singlePhone != "null") { + if (!phonesList.contains(singlePhone.trim())) { + phonesList.add(singlePhone.trim()); + } + } + return WalaPlusRetailerContact( + email: singleEmail, emails: emailsList, + phone: singlePhone, phones: phonesList, - website: json['website']?.toString(), + website: WalaPlusRetailerModel.decodeUnicodeString(json['website']), + address: WalaPlusRetailerModel.decodeUnicodeString(json['address']), + instagram: json['instagram']?.toString(), + twitter: json['twitter']?.toString(), ); } Map toJson() { return { + 'email': email, 'emails': emails, + 'phone': phone, 'phones': phones, 'website': website, + 'address': address, + 'instagram': instagram, + 'twitter': twitter, }; } + + String? get primaryEmail { + if (email != null && email!.trim().isNotEmpty && email != "null") return email!.trim(); + if (emails != null && emails!.isNotEmpty) { + for (var e in emails!) { + if (e.trim().isNotEmpty && e != "null") return e.trim(); + } + } + return null; + } + + String? get primaryPhone { + if (phone != null && phone!.trim().isNotEmpty && phone != "null") return phone!.trim(); + if (phones != null && phones!.isNotEmpty) { + for (var p in phones!) { + if (p.trim().isNotEmpty && p != "null") return p.trim(); + } + } + return null; + } } class WalaPlusRetailerLocation { + dynamic id; String? address; String? city; String? cityNameAr; @@ -261,6 +753,7 @@ class WalaPlusRetailerLocation { dynamic placeId; WalaPlusRetailerLocation({ + this.id, this.address, this.city, this.cityNameAr, @@ -275,13 +768,14 @@ class WalaPlusRetailerLocation { factory WalaPlusRetailerLocation.fromJson(Map json) { return WalaPlusRetailerLocation( - address: json['address']?.toString(), - city: json['city']?.toString(), - cityNameAr: json['city_name_ar']?.toString(), - cityNameEn: json['city_name_en']?.toString(), + id: json['id'], + address: WalaPlusRetailerModel.decodeUnicodeString(json['address']), + city: WalaPlusRetailerModel.decodeUnicodeString(json['city']), + cityNameAr: WalaPlusRetailerModel.decodeUnicodeString(json['city_name_ar']), + cityNameEn: WalaPlusRetailerModel.decodeUnicodeString(json['city_name_en']), contact: json['contact']?.toString(), - countryNameAr: json['country_name_ar']?.toString(), - countryNameEn: json['country_name_en']?.toString(), + countryNameAr: WalaPlusRetailerModel.decodeUnicodeString(json['country_name_ar']), + countryNameEn: WalaPlusRetailerModel.decodeUnicodeString(json['country_name_en']), latitude: json['latitude'] is num ? (json['latitude'] as num).toDouble() : double.tryParse(json['latitude']?.toString() ?? ''), longitude: json['longitude'] is num ? (json['longitude'] as num).toDouble() : double.tryParse(json['longitude']?.toString() ?? ''), placeId: json['place_id'], @@ -289,7 +783,8 @@ class WalaPlusRetailerLocation { } Map toJson() { - Map data = {}; + Map data = {}; + data['id'] = id; data['address'] = address; data['city'] = city; data['city_name_ar'] = cityNameAr; @@ -302,4 +797,95 @@ class WalaPlusRetailerLocation { data['place_id'] = placeId; return data; } + + String getLocalizedCity(bool isAr) { + if (isAr) { + if (cityNameAr != null && cityNameAr!.trim().isNotEmpty && cityNameAr != "null") { + return cityNameAr!.trim(); + } + } else { + if (cityNameEn != null && cityNameEn!.trim().isNotEmpty && cityNameEn != "null") { + return cityNameEn!.trim(); + } + } + if (city != null && city!.trim().isNotEmpty && city != "null") { + return city!.trim(); + } + return isAr ? (cityNameAr ?? "") : (cityNameEn ?? ""); + } + + String getLocalizedCountry(bool isAr) { + if (isAr) { + if (countryNameAr != null && countryNameAr!.trim().isNotEmpty && countryNameAr != "null") { + return countryNameAr!.trim(); + } + } else { + if (countryNameEn != null && countryNameEn!.trim().isNotEmpty && countryNameEn != "null") { + return countryNameEn!.trim(); + } + } + return ""; + } +} + +class WalaPlusOfferModel { + String? id; + String? retailerId; + String? title; + String? image; + String? imageThumb; + String? type; + String? description; + dynamic price; + dynamic usage; + String? expiredAt; + + WalaPlusOfferModel({ + this.id, + this.retailerId, + this.title, + this.image, + this.imageThumb, + this.type, + this.description, + this.price, + this.usage, + this.expiredAt, + }); + + factory WalaPlusOfferModel.fromJson(Map json) { + return WalaPlusOfferModel( + id: json['id']?.toString(), + retailerId: (json['retailer_id'] ?? json['retailerId'] ?? json['retailerID'])?.toString(), + title: WalaPlusRetailerModel.decodeUnicodeString(json['title']), + image: json['image']?.toString(), + imageThumb: (json['image_thumb'] ?? json['imageThumb'])?.toString(), + type: json['type']?.toString(), + description: WalaPlusRetailerModel.decodeUnicodeString(json['description']), + price: json['price'], + usage: json['usage'], + expiredAt: (json['expired_at'] ?? json['expiredAt'] ?? json['expiry_date'])?.toString(), + ); + } + + Map toJson() { + return { + 'id': id, + 'retailer_id': retailerId, + 'title': title, + 'image': image, + 'image_thumb': imageThumb, + 'type': type, + 'description': description, + 'price': price, + 'usage': usage, + 'expired_at': expiredAt, + }; + } + + String? get displayImage { + if (image != null && image!.trim().isNotEmpty && image != "null") return image!.trim(); + if (imageThumb != null && imageThumb!.trim().isNotEmpty && imageThumb != "null") return imageThumb!.trim(); + return null; + } } diff --git a/lib/models/post_params_model.dart b/lib/models/post_params_model.dart index 05356c2..ac2a690 100644 --- a/lib/models/post_params_model.dart +++ b/lib/models/post_params_model.dart @@ -40,7 +40,7 @@ class PostParamsModel { mobileType = json['MobileType']; logInTokenID = json['LogInTokenID']; payrollCodeStr = json['PayrollCodeStr']; - pLegislationCode = json['LegislationCodeStr']; + pLegislationCode = json['LegislationCodeStr']?.toString().trim().toUpperCase(); tokenID = json['TokenID']; } @@ -92,7 +92,7 @@ class PostParamsModel { set setPSelectedEmployeeNumber(String? v) => pSelectedEmployeeNumber = v; - set setPLegislationCode(String? v) => pLegislationCode = v; + set setPLegislationCode(String? v) => pLegislationCode = v?.trim().toUpperCase(); set setPayrollCodeStr(String? v) => payrollCodeStr = v; } diff --git a/lib/models/profile/patient_info_model.dart b/lib/models/profile/patient_info_model.dart new file mode 100644 index 0000000..1940f08 --- /dev/null +++ b/lib/models/profile/patient_info_model.dart @@ -0,0 +1,139 @@ +class PatientInfoModel { + int? age; + String? dateOfBirth; + String? firstNameAr; + String? firstNameEn; + String? secondNameAr; + String? secondNameEn; + String? thirdNameAr; + String? thirdNameEn; + String? lastNameAr; + String? lastNameEn; + String? gender; + String? healthId; + String? idNumber; + String? idType; + bool? isHijri; + String? maritalStatus; + String? maritalStatusCode; + String? nationalDateOfBirth; + String? nationality; + String? nationalityCode; + String? occupation; + String? patientStatus; + String? placeOfBirth; + int? messageStatus; + String? errorEndUserMessage; + String? errorMessage; + + PatientInfoModel({ + this.age, + this.dateOfBirth, + this.firstNameAr, + this.firstNameEn, + this.secondNameAr, + this.secondNameEn, + this.thirdNameAr, + this.thirdNameEn, + this.lastNameAr, + this.lastNameEn, + this.gender, + this.healthId, + this.idNumber, + this.idType, + this.isHijri, + this.maritalStatus, + this.maritalStatusCode, + this.nationalDateOfBirth, + this.nationality, + this.nationalityCode, + this.occupation, + this.patientStatus, + this.placeOfBirth, + this.messageStatus, + this.errorEndUserMessage, + this.errorMessage, + }); + + factory PatientInfoModel.fromJson(Map json) { + return PatientInfoModel( + age: json['Age'], + dateOfBirth: json['DateOfBirth'], + firstNameAr: json['FirstNameAr'], + firstNameEn: json['FirstNameEn'], + secondNameAr: json['SecondNameAr'], + secondNameEn: json['SecondNameEn'], + thirdNameAr: json['ThirdNameAr'], + thirdNameEn: json['ThirdNameEn'], + lastNameAr: json['LastNameAr'], + lastNameEn: json['LastNameEn'], + gender: json['Gender'], + healthId: json['HealthId'], + idNumber: json['IdNumber'], + idType: json['IdType'], + isHijri: json['IsHijri'], + maritalStatus: json['MaritalStatus'], + maritalStatusCode: json['MaritalStatusCode'], + nationalDateOfBirth: json['NationalDateOfBirth'], + nationality: json['Nationality'], + nationalityCode: json['NationalityCode'], + occupation: json['Occupation'], + patientStatus: json['PatientStatus']?.toString(), + placeOfBirth: json['PlaceofBirth'], + messageStatus: json['MessageStatus'], + errorEndUserMessage: json['ErrorEndUserMessage'], + errorMessage: json['ErrorMessage'], + ); + } + + Map toJson() { + return { + 'Age': age, + 'DateOfBirth': dateOfBirth, + 'FirstNameAr': firstNameAr, + 'FirstNameEn': firstNameEn, + 'SecondNameAr': secondNameAr, + 'SecondNameEn': secondNameEn, + 'ThirdNameAr': thirdNameAr, + 'ThirdNameEn': thirdNameEn, + 'LastNameAr': lastNameAr, + 'LastNameEn': lastNameEn, + 'Gender': gender, + 'HealthId': healthId, + 'IdNumber': idNumber, + 'IdType': idType, + 'IsHijri': isHijri, + 'MaritalStatus': maritalStatus, + 'MaritalStatusCode': maritalStatusCode, + 'NationalDateOfBirth': nationalDateOfBirth, + 'Nationality': nationality, + 'NationalityCode': nationalityCode, + 'Occupation': occupation, + 'PatientStatus': patientStatus, + 'PlaceofBirth': placeOfBirth, + 'MessageStatus': messageStatus, + 'ErrorEndUserMessage': errorEndUserMessage, + 'ErrorMessage': errorMessage, + }; + } + + String get fullNameAr { + List parts = [ + firstNameAr ?? '', + secondNameAr ?? '', + (thirdNameAr != null && thirdNameAr != '-') ? thirdNameAr! : '', + lastNameAr ?? '', + ].where((s) => s.trim().isNotEmpty && s != '-').toList(); + return parts.join(' ').trim(); + } + + String get fullNameEn { + List parts = [ + firstNameEn ?? '', + secondNameEn ?? '', + (thirdNameEn != null && thirdNameEn != '-') ? thirdNameEn! : '', + lastNameEn ?? '', + ].where((s) => s.trim().isNotEmpty && s != '-').toList(); + return parts.join(' ').trim(); + } +} diff --git a/lib/provider/chat_call_provider.dart b/lib/provider/chat_call_provider.dart index 45205df..5eb3964 100644 --- a/lib/provider/chat_call_provider.dart +++ b/lib/provider/chat_call_provider.dart @@ -17,12 +17,12 @@ class ChatCallProvider with ChangeNotifier, DiagnosticableTreeMixin { MediaStream? _remoteStream; void initCallListeners() { - chatHubConnection.on("OnCallAcceptedAsync", onCallAcceptedAsync); - chatHubConnection.on("OnIceCandidateAsync", onIceCandidateAsync); - chatHubConnection.on("OnOfferAsync", onOfferAsync); - chatHubConnection.on("OnAnswerOffer", onAnswerOffer); - chatHubConnection.on("OnHangUpAsync", onHangUpAsync); - chatHubConnection.on("OnCallDeclinedAsync", onCallDeclinedAsync); + chatHubConnection?.on("OnCallAcceptedAsync", onCallAcceptedAsync); + chatHubConnection?.on("OnIceCandidateAsync", onIceCandidateAsync); + chatHubConnection?.on("OnOfferAsync", onOfferAsync); + chatHubConnection?.on("OnAnswerOffer", onAnswerOffer); + chatHubConnection?.on("OnHangUpAsync", onHangUpAsync); + chatHubConnection?.on("OnCallDeclinedAsync", onCallDeclinedAsync); } //Video Constraints @@ -175,13 +175,13 @@ class ChatCallProvider with ChangeNotifier, DiagnosticableTreeMixin { args = [targetUserID, data]; //json In Data } - await chatHubConnection.invoke(invokeMethod, args: args); + await chatHubConnection?.invoke(invokeMethod, args: args); } void stopListeners() async { - chatHubConnection.off('OnCallDeclinedAsync'); - chatHubConnection.off('OnCallAcceptedAsync'); - chatHubConnection.off('OnIceCandidateAsync'); - chatHubConnection.off('OnAnswerOffer'); + chatHubConnection?.off('OnCallDeclinedAsync'); + chatHubConnection?.off('OnCallAcceptedAsync'); + chatHubConnection?.off('OnIceCandidateAsync'); + chatHubConnection?.off('OnAnswerOffer'); } } diff --git a/lib/provider/chat_provider_model.dart b/lib/provider/chat_provider_model.dart index 4c19817..e0f5900 100644 --- a/lib/provider/chat_provider_model.dart +++ b/lib/provider/chat_provider_model.dart @@ -113,16 +113,16 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { Future buildHubConnection() async { chatHubConnection = await getHubConnection(); - await chatHubConnection.start(); + await chatHubConnection?.start(); if (kDebugMode) { logger.i("Hub Conn: Startedddddddd"); } - chatHubConnection.on("OnDeliveredChatUserAsync", onMsgReceived); - chatHubConnection.on("OnGetChatConversationCount", onNewChatConversion); + chatHubConnection?.on("OnDeliveredChatUserAsync", onMsgReceived); + chatHubConnection?.on("OnGetChatConversationCount", onNewChatConversion); //group On message - chatHubConnection.on("OnDeliveredGroupChatHistoryAsync", onGroupMsgReceived); + chatHubConnection?.on("OnDeliveredGroupChatHistoryAsync", onGroupMsgReceived); } Future getHubConnection() async { @@ -135,16 +135,16 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { } void registerEvents() { - chatHubConnection.on("OnUpdateUserStatusAsync", changeStatus); - // chatHubConnection.on("OnDeliveredChatUserAsync", onMsgReceived); + chatHubConnection?.on("OnUpdateUserStatusAsync", changeStatus); + // chatHubConnection?.on("OnDeliveredChatUserAsync", onMsgReceived); - chatHubConnection.on("OnSubmitChatAsync", OnSubmitChatAsync); - chatHubConnection.on("OnUserTypingAsync", onUserTyping); - chatHubConnection.on("OnUserCountAsync", userCountAsync); - // chatHubConnection.on("OnUpdateUserChatHistoryWindowsAsync", updateChatHistoryWindow); - chatHubConnection.on("OnGetUserChatHistoryNotDeliveredAsync", chatNotDelivered); - chatHubConnection.on("OnUpdateUserChatHistoryStatusAsync", updateUserChatStatus); - chatHubConnection.on("OnGetGroupUserStatusAsync", getGroupUserStatus); + chatHubConnection?.on("OnSubmitChatAsync", OnSubmitChatAsync); + chatHubConnection?.on("OnUserTypingAsync", onUserTyping); + chatHubConnection?.on("OnUserCountAsync", userCountAsync); + // chatHubConnection?.on("OnUpdateUserChatHistoryWindowsAsync", updateChatHistoryWindow); + chatHubConnection?.on("OnGetUserChatHistoryNotDeliveredAsync", chatNotDelivered); + chatHubConnection?.on("OnUpdateUserChatHistoryStatusAsync", updateUserChatStatus); + chatHubConnection?.on("OnGetGroupUserStatusAsync", getGroupUserStatus); // // {"type":1,"target":"","arguments":[[{"id":217869,"userName":"Sultan.Khan","email":"Sultan.Khan@cloudsolutions.com.sa","phone":null,"title":"Sultan.Khan","userStatus":1,"image":null,"unreadMessageCount":0,"userAction":3,"isPin":false,"isFav":false,"isAdmin":false,"rKey":null,"totalCount":0,"isHuaweiDevice":false,"deviceToken":null},{"id":15153,"userName":"Tamer.Fanasheh","email":"Tamer.F@cloudsolutions.com.sa","phone":null,"title":"Tamer Fanasheh","userStatus":2,"image":null,"unreadMessageCount":0,"userAction":3,"isPin":false,"isFav":false,"isAdmin":true,"rKey":null,"totalCount":0,"isHuaweiDevice":false,"deviceToken":null}]]} @@ -183,7 +183,7 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { } Future invokeUserChatHistoryNotDeliveredAsync({required int userId}) async { - await chatHubConnection.invoke("GetUserChatHistoryNotDeliveredAsync", args: [userId]); + await chatHubConnection?.invoke("GetUserChatHistoryNotDeliveredAsync", args: [userId]); return ""; } @@ -252,7 +252,7 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { void updateUserChatHistoryStatusAsync(List data) { try { - chatHubConnection.invoke("UpdateUserChatHistoryStatusAsync", args: [data]); + chatHubConnection?.invoke("UpdateUserChatHistoryStatusAsync", args: [data]); } catch (e) { throw e; } @@ -260,7 +260,7 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { void updateUserChatHistoryOnMsg(List data) { try { - chatHubConnection.invoke("UpdateUserChatHistoryStatusAsync", args: [data]); + chatHubConnection?.invoke("UpdateUserChatHistoryStatusAsync", args: [data]); } catch (e) { throw e; } @@ -753,7 +753,7 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { String chatData = '{"contant":"$msg","contantNo":"$contentNo","chatEventId":$chatEventId,"fileTypeId": $fileTypeId,"currentUserId":${AppState().chatDetails!.response!.id},"chatSource":1,"userChatHistoryLineRequestList":[{"isSeen":false,"isDelivered":false,"targetUserId":$targetUserId,"targetUserStatus":1}],"chatReplyId":$chatReplyId,"conversationId":"$chatCID"}'; - await chatHubConnection.invoke("AddChatUserAsync", args: [json.decode(chatData)]); + await chatHubConnection?.invoke("AddChatUserAsync", args: [json.decode(chatData)]); } //groupChatMessage @@ -829,7 +829,7 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { String chatData = '{"contant":"$msg","contantNo":"$contentNo","chatEventId":$chatEventId,"fileTypeId":$fileTypeId,"currentUserId":${AppState().chatDetails!.response!.id},"chatSource":1,"groupId":$targetGroupId,"groupChatHistoryLineRequestList":${json.encode(targetUsers)},"chatReplyId": $chatReplyId,"conversationId":"${uuid.v4()}"}'; - await chatHubConnection.invoke("AddGroupChatHistoryAsync", args: [json.decode(chatData)]); + await chatHubConnection?.invoke("AddGroupChatHistoryAsync", args: [json.decode(chatData)]); } void sendGroupChatMessage( @@ -1450,7 +1450,14 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { pChatHistory?.clear(); uGroups?.clear(); searchGroup?.clear(); - chatHubConnection.stop(); + try { + chatHubConnection?.stop(); + } catch (e) { + if (kDebugMode) { + print("Error stopping chatHubConnection: $e"); + } + } + chatHubConnection = null; AppState().chatDetails = null; } } @@ -1582,17 +1589,17 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { } Future invokeChatCounter({required int userId}) async { - await chatHubConnection.invoke("GetChatCounversationCount", args: [userId]); + await chatHubConnection?.invoke("GetChatCounversationCount", args: [userId]); return ""; } void userTypingInvoke({required int currentUser, required int reciptUser}) async { - await chatHubConnection.invoke("UserTypingAsync", args: [reciptUser, currentUser]); + await chatHubConnection?.invoke("UserTypingAsync", args: [reciptUser, currentUser]); } void groupTypingInvoke({required GroupResponse groupDetails, required int groupId}) async { var data = json.decode(json.encode(groupDetails.groupUserList)); - await chatHubConnection.invoke("GroupTypingAsync", args: ["${groupDetails.adminUser!.userName}", data, groupId]); + await chatHubConnection?.invoke("GroupTypingAsync", args: ["${groupDetails.adminUser!.userName}", data, groupId]); } //////// Audio Recoding Work //////////////////// diff --git a/lib/ui/chat/chat_home.dart b/lib/ui/chat/chat_home.dart index c522475..7dc63ae 100644 --- a/lib/ui/chat/chat_home.dart +++ b/lib/ui/chat/chat_home.dart @@ -33,7 +33,7 @@ class _ChatHomeState extends State { void initState() { super.initState(); - if (chatHubConnection.state == HubConnectionState.Connected) { + if (chatHubConnection?.state == HubConnectionState.Connected) { data = Provider.of(context, listen: false); data.registerEvents(); } @@ -46,7 +46,7 @@ class _ChatHomeState extends State { } void fetchAgain() { - if (chatHubConnection.state != HubConnectionState.Connected) { + if (chatHubConnection?.state != HubConnectionState.Connected) { data.getUserAutoLoginToken().whenComplete(() async { await data.buildHubConnection(); data.getUserRecentChats(); diff --git a/lib/ui/landing/dashboard_screen.dart b/lib/ui/landing/dashboard_screen.dart index 2f4147d..65165aa 100644 --- a/lib/ui/landing/dashboard_screen.dart +++ b/lib/ui/landing/dashboard_screen.dart @@ -29,13 +29,13 @@ import 'package:mohem_flutter_app/ui/marathon/widgets/marathon_banner.dart'; import 'package:mohem_flutter_app/widgets/bottom_sheet.dart'; import 'package:mohem_flutter_app/widgets/mark_attendance_widget.dart'; import 'package:mohem_flutter_app/widgets/shimmer/dashboard_shimmer_widget.dart'; -import 'package:mohem_flutter_app/widgets/shimmer/offers_shimmer_widget.dart'; + import 'package:provider/provider.dart'; import 'package:pull_to_refresh/pull_to_refresh.dart'; import 'package:signalr_netcore/signalr_client.dart'; import 'package:url_launcher/url_launcher.dart'; -late HubConnection chatHubConnection; +HubConnection? chatHubConnection; class DashboardScreen extends StatefulWidget { DashboardScreen({Key? key}) : super(key: key); @@ -56,7 +56,7 @@ class _DashboardScreenState extends State with WidgetsBindingOb int currentIndex = 0; - bool isDisplayMazaya = false; + bool isDisplayMazaya = true; @override void initState() { @@ -93,7 +93,7 @@ class _DashboardScreenState extends State with WidgetsBindingOb WidgetsBinding.instance.removeObserver(this); super.dispose(); if (!cProvider.disbaleChatForThisUser) { - chatHubConnection.stop(); + chatHubConnection?.stop(); } } @@ -120,16 +120,16 @@ class _DashboardScreenState extends State with WidgetsBindingOb Future checkHubCon() async { // chatHubConnection = await context.read().getHubConnection(); - if (chatHubConnection.state == HubConnectionState.Connected) { - await chatHubConnection.stop(); - await chatHubConnection.start(); - } else if (chatHubConnection.state != HubConnectionState.Connected) { - await chatHubConnection.start(); + if (chatHubConnection?.state == HubConnectionState.Connected) { + await chatHubConnection?.stop(); + await chatHubConnection?.start(); + } else if (chatHubConnection != null && chatHubConnection!.state != HubConnectionState.Connected) { + await chatHubConnection?.start(); } } void gotoChat(BuildContext context) async { - if (chatHubConnection.state == HubConnectionState.Connected) { + if (chatHubConnection?.state == HubConnectionState.Connected) { Utils.hideLoading(context); Navigator.pushNamed(context, AppRoutes.chat); String isAppOpendByChat = await Utils.getStringFromPrefs("isAppOpendByChat"); @@ -453,141 +453,32 @@ class _DashboardScreenState extends State with WidgetsBindingOb return const SizedBox(); } }, - ).paddingOnly(bottom: 24, left: 24, right: 24), + ).paddingOnly(bottom: 18, left: 24, right: 24), // eventActivityWidget(context), if (isDisplayMazaya) ...[ - Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Directionality( - textDirection: AppState().isArabic(context) ? ui.TextDirection.rtl : ui.TextDirection.ltr, - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - gradient: const LinearGradient(colors: [Color(0xFF91C481), Color(0xFF7CCED7)], begin: Alignment.centerLeft, end: Alignment.centerRight), - ), - child: Padding( - padding: const EdgeInsets.all(3.0), // This creates the border width - child: Container( - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(17), // Slightly less than outer radius - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - flex: 4, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Row( - children: [ - Expanded( - flex: 2, - child: RichText( - text: - AppState().isArabic(context) - ? TextSpan( - children: [ - TextSpan( - text: 'اطلع على مميزات', - style: TextStyle( - fontSize: 16, - letterSpacing: -0.2, - fontFamily: AppState().isArabic(context) ? 'Cairo' : 'Poppins', - fontWeight: FontWeight.w700, - height: 24 / 16, - color: const Color(0xFF5D5E5E), - ), - ), - TextSpan( - text: ' مزايا', - style: TextStyle( - fontSize: 16, - fontFamily: AppState().isArabic(context) ? 'Cairo' : 'Poppins', - fontWeight: FontWeight.w700, - letterSpacing: -0.2, - height: 24 / 16, - color: MyColors.mazayaRedColor, // Use your MAZAYA red color here if defined, e.g. MyColors.mazayaRed - ), - ), - ], - ) - : TextSpan( - children: [ - TextSpan( - text: LocaleKeys.explore.tr() + ' ', - style: const TextStyle( - fontSize: 16, - letterSpacing: -0.2, - fontFamily: 'Poppins', - fontWeight: FontWeight.w700, - height: 24 / 16, - color: Color(0xFF5D5E5E), - ), - ), - TextSpan( - text: LocaleKeys.mazaya.tr(), - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w700, - fontFamily: 'Poppins', - letterSpacing: -0.2, - height: 24 / 16, - color: MyColors.mazayaRedColor, // Use your MAZAYA red color here if defined, e.g. MyColors.mazayaRed - ), - ), - TextSpan( - text: ' ' + LocaleKeys.benefits.tr(), - style: const TextStyle( - fontSize: 16, - letterSpacing: -0.2, - fontFamily: 'Poppins', - fontWeight: FontWeight.w700, - height: 24 / 16, - color: Color(0xFF5D5E5E), - ), - ), - ], - ), - ), - ), - const Expanded(flex: 1, child: SizedBox()), - ], - ), - const SizedBox(height: 8), - LocaleKeys.mazayaDesc.tr().toText11(color: const Color(0xFF5D5E5E)), - ], - ), - ), - Expanded( - flex: 2, - child: Column( - crossAxisAlignment: CrossAxisAlignment.end, - mainAxisAlignment: MainAxisAlignment.end, - children: [ - SvgPicture.asset("assets/icons/mazaya_brand.svg", width: 90, height: 47), - const SizedBox(height: 28), - LocaleKeys.viewallofferMazaya.tr().toText12(isUnderLine: true, color: const Color(0xFF3B3D4A)).onPress(() { - Navigator.pushNamed(context, AppRoutes.offersAndDiscounts); - }), - ], - ), - ), - ], - ).paddingOnly(left: 21, right: 21, top: 14, bottom: 14), - ), - ), - ).paddingOnly(left: 21, right: 21, top: 0, bottom: 21), + InkWell( + borderRadius: BorderRadius.circular(20), + onTap: () { + Navigator.pushNamed(context, AppRoutes.dashboardScreenForDependet); + }, + child: ClipRRect( + borderRadius: BorderRadius.circular(20), + child: AspectRatio( + aspectRatio: 386 / 121.37, + child: SvgPicture.asset( + AppState().isArabic(context) + ? "assets/images/mazaya_banner_ar.svg" + : "assets/images/mazaya_banner_en.svg", + width: double.infinity, + fit: BoxFit.fill, + ), ), - ], - ), + ), + ).paddingOnly(left: 21, right: 21, top: 0, bottom: 21), ], + // Offers & Discounts commented out as requested + /* Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, @@ -678,6 +569,7 @@ class _DashboardScreenState extends State with WidgetsBindingOb ), ], ), + */ Container( width: double.infinity, diff --git a/lib/ui/landing/dashboard_screen_for_dependet.dart b/lib/ui/landing/dashboard_screen_for_dependet.dart index 0d7635a..49fb552 100644 --- a/lib/ui/landing/dashboard_screen_for_dependet.dart +++ b/lib/ui/landing/dashboard_screen_for_dependet.dart @@ -1,5 +1,8 @@ import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:mohem_flutter_app/api/login_api_client.dart'; import 'package:mohem_flutter_app/api/offers_and_discounts_api_client.dart'; import 'package:mohem_flutter_app/app_state/app_state.dart'; import 'package:mohem_flutter_app/classes/colors.dart'; @@ -8,12 +11,10 @@ import 'package:mohem_flutter_app/classes/utils.dart'; import 'package:mohem_flutter_app/config/routes.dart'; import 'package:mohem_flutter_app/extensions/int_extensions.dart'; import 'package:mohem_flutter_app/extensions/string_extensions.dart'; -import 'package:mohem_flutter_app/extensions/widget_extensions.dart'; import 'package:mohem_flutter_app/generated/locale_keys.g.dart'; import 'package:mohem_flutter_app/models/offers_and_discounts/wala_plus_category_model.dart'; import 'package:mohem_flutter_app/models/offers_and_discounts/wala_plus_retailer_model.dart'; import 'package:shimmer/shimmer.dart'; -import 'package:url_launcher/url_launcher.dart'; class DashboardScreenForDependet extends StatefulWidget { const DashboardScreenForDependet({Key? key}) : super(key: key); @@ -26,7 +27,8 @@ class _DashboardScreenForDependetState extends State List retailersList = []; List _filteredRetailersList = []; List categoriesList = []; - String currentCategoryId = "all"; + String currentCategoryId = ""; + String selectedTypeFilter = "all"; bool isNearestOffers = true; bool isLoading = true; bool isLoadingMore = false; @@ -39,18 +41,107 @@ class _DashboardScreenForDependetState extends State String searchQuery = ""; final TextEditingController _searchController = TextEditingController(); final ScrollController _scrollController = ScrollController(); + bool _hasRegisteredMobileLoginInfo = false; + + List get _availableTypeFilters { + Set types = {}; + for (WalaPlusRetailerModel r in retailersList) { + String? t = r.normalizedType; + if (t != null && t.isNotEmpty) { + types.add(t); + } + } + List list = ['all', 'instore', 'online']; + for (String t in types) { + if (!list.contains(t)) { + list.add(t); + } + } + return list; + } + + String _getTypeFilterTitle(String type) { + if (type == 'all') { + return LocaleKeys.allCategories.tr(); + } else if (type == 'instore') { + return LocaleKeys.inStore.tr(); + } else if (type == 'online') { + return LocaleKeys.online.tr(); + } + return type.isNotEmpty ? (type[0].toUpperCase() + type.substring(1)) : type; + } + + IconData _getTypeFilterIcon(String type) { + if (type == 'instore') { + return Icons.storefront_rounded; + } else if (type == 'online') { + return Icons.language_rounded; + } + return Icons.tune_rounded; + } @override void initState() { super.initState(); _scrollController.addListener(_scrollListener); + _loadDependentName(); WidgetsBinding.instance.addPostFrameCallback((_) async { if (mounted) { + await _registerMobileLoginInfo(); await fetchCategories(); + await fetchOffers(categoryId: currentCategoryId.isEmpty ? "all" : currentCategoryId); } }); } + Future _registerMobileLoginInfo() async { + if (_hasRegisteredMobileLoginInfo) return; + if (!ApiConsts.isDependetLogin) return; + _hasRegisteredMobileLoginInfo = true; + + try { + int loginType = AppState().dependentLoginType ?? 1; + String nationalId = AppState().getUserName ?? ""; + String dob = AppState().dependentDob ?? ""; + String name = AppState().dependentName ?? ""; + String mobile = AppState().postParamsObject?.mobileNumber ?? ""; + String tokenID = (AppState().postParamsObject?.tokenID != null && AppState().postParamsObject!.tokenID!.isNotEmpty) + ? AppState().postParamsObject!.tokenID! + : (AppState().authenticationTokenID ?? ""); + if (tokenID.isEmpty) { + tokenID = await Utils.getStringFromPrefs("P_DEP_TOKEN_ID"); + } + int sessionId = AppState().postParamsObject?.pSessionId ?? 0; + String email = AppState().postParamsObject?.pEmailAddress ?? ""; + String deviceToken = AppState().getIsHuawei + ? AppState().getHuaweiPushToken + : (AppState().getDeviceToken ?? ""); + + await LoginApiClient().insertMobileLoginInfoDependent( + loginType: loginType, + nationalId: nationalId, + dob: dob, + dependentName: name, + mobileNumber: mobile, + tokenID: tokenID, + sessionId: sessionId, + email: email, + deviceToken: deviceToken, + context: context, + ); + } catch (e) { + if (kDebugMode) { + print("Error calling insertMobileLoginInfoDependent: $e"); + } + } + } + + void _loadDependentName() { + if (AppState().dependentName != null && AppState().dependentName!.isNotEmpty && mounted) { + setState(() {}); + } + } + @override void dispose() { _scrollController.removeListener(_scrollListener); @@ -78,25 +169,64 @@ class _DashboardScreenForDependetState extends State try { List list = await OffersAndDiscountsApiClient().getWalaPlusCategories(context: context); - bool isAr = mounted ? AppState().isArabic(context) : false; - WalaPlusCategoryModel allCategory = WalaPlusCategoryModel( - id: "all", - name: isAr ? "الكل" : "All", - ); + if (list.isEmpty) { + await Future.delayed(const Duration(milliseconds: 600)); + list = await OffersAndDiscountsApiClient().getWalaPlusCategories(context: context); + } if (mounted) { + List fullList = []; + bool hasAll = list.any((c) => (c.id == "all" || c.name?.toLowerCase() == "all" || c.name == "الجميع" || c.name == "الكل")); + if (!hasAll) { + fullList.add(WalaPlusCategoryModel( + id: "all", + name: LocaleKeys.allCategories.tr(), + )); + } + fullList.addAll(list); + setState(() { - categoriesList = [allCategory, ...list]; - currentCategoryId = "all"; + categoriesList = fullList; + if (currentCategoryId.isEmpty) { + currentCategoryId = "all"; + } }); } } catch (e) { - print("Error fetching WalaPlus categories: $e"); + debugPrint("Error fetching WalaPlus categories (attempt 1): $e"); + try { + await Future.delayed(const Duration(milliseconds: 600)); + List retryList = await OffersAndDiscountsApiClient().getWalaPlusCategories(context: context); + if (mounted && retryList.isNotEmpty) { + List fullList = []; + bool hasAll = retryList.any((c) => (c.id == "all" || c.name?.toLowerCase() == "all" || c.name == "الجميع" || c.name == "الكل")); + if (!hasAll) { + fullList.add(WalaPlusCategoryModel( + id: "all", + name: LocaleKeys.allCategories.tr(), + )); + } + fullList.addAll(retryList); + setState(() { + categoriesList = fullList; + if (currentCategoryId.isEmpty) { + currentCategoryId = "all"; + } + }); + return; + } + } catch (retryEx) { + debugPrint("Error fetching WalaPlus categories (attempt 2): $retryEx"); + } + if (mounted && categoriesList.isEmpty) { - bool isAr = AppState().isArabic(context); setState(() { - categoriesList = [WalaPlusCategoryModel(id: "all", name: isAr ? "الكل" : "All")]; - currentCategoryId = "all"; + categoriesList = [ + WalaPlusCategoryModel(id: "all", name: LocaleKeys.allCategories.tr()), + ]; + if (currentCategoryId.isEmpty) { + currentCategoryId = "all"; + } }); } } finally { @@ -104,16 +234,12 @@ class _DashboardScreenForDependetState extends State setState(() { isCategoriesLoading = false; }); - // Default call without category filter ("all" -> passes "") - fetchOffers(categoryId: "all"); } } } Future fetchOffers({String? categoryId}) async { - String targetCategoryId = (categoryId != null && categoryId.isNotEmpty) - ? categoryId - : currentCategoryId; + String targetCategoryId = categoryId ?? (currentCategoryId.isEmpty ? "all" : currentCategoryId); setState(() { isLoading = true; @@ -122,7 +248,7 @@ class _DashboardScreenForDependetState extends State }); try { - String catParam = (targetCategoryId == "all") ? "" : targetCategoryId; + String catParam = (targetCategoryId == "all" || targetCategoryId == "0") ? "" : targetCategoryId; print("Fetching WalaPlus retailers: category='$catParam', page=1, per_page=$pageSize"); WalaPlusRetailersResponse? response = await OffersAndDiscountsApiClient().getWalaPlusRetailers( categoryId: catParam, @@ -161,7 +287,7 @@ class _DashboardScreenForDependetState extends State try { int nextPage = currentPage + 1; - String catParam = (currentCategoryId == "all") ? "" : currentCategoryId; + String catParam = (currentCategoryId == "all" || currentCategoryId == "0") ? "" : currentCategoryId; print("Fetching WalaPlus retailers next page: $nextPage of $totalPages, category: '$catParam'"); @@ -196,15 +322,34 @@ class _DashboardScreenForDependetState extends State } Future _onRefresh() async { - await fetchCategories(); + await Future.wait([ + fetchCategories(), + fetchOffers(categoryId: currentCategoryId.isEmpty ? "all" : currentCategoryId), + ]); } - void _runFilter(String query) { - searchQuery = query; + void _runFilter([String? query]) { + if (query != null) { + searchQuery = query; + } List temp = retailersList; - if (query.trim().isNotEmpty) { - String q = query.trim().toLowerCase(); - temp = temp.where((item) { + + if (selectedTypeFilter != "all" && selectedTypeFilter.isNotEmpty) { + temp = temp.where((WalaPlusRetailerModel item) { + String? norm = item.normalizedType; + if (selectedTypeFilter == "online") { + return norm == "online"; + } else if (selectedTypeFilter == "instore") { + return norm == "instore"; + } else { + return norm == selectedTypeFilter || (item.type?.trim().toLowerCase() == selectedTypeFilter); + } + }).toList(); + } + + if (searchQuery.trim().isNotEmpty) { + String q = searchQuery.trim().toLowerCase(); + temp = temp.where((WalaPlusRetailerModel item) { String name = (item.name ?? "").toLowerCase(); String title = (item.offerTitle ?? "").toLowerCase(); String desc = (item.description ?? "").toLowerCase(); @@ -216,20 +361,7 @@ class _DashboardScreenForDependetState extends State setState(() {}); } - String _getUserDisplayName() { - if (AppState().memberInformationList?.eMPLOYEENAME != null && - AppState().memberInformationList!.eMPLOYEENAME!.trim().isNotEmpty) { - return AppState().memberInformationList!.eMPLOYEENAME!.trim(); - } - bool isAr = AppState().isArabic(context); - String? dispName = isAr - ? AppState().memberInformationList?.eMPLOYEEDISPLAYNAMEAr - : AppState().memberInformationList?.eMPLOYEEDISPLAYNAMEEn; - if (dispName != null && dispName.trim().isNotEmpty) { - return dispName.trim(); - } - return "Test User"; - } + void _openRetailer(WalaPlusRetailerModel item) { Navigator.pushNamed( @@ -242,7 +374,7 @@ class _DashboardScreenForDependetState extends State void _performLogout() { Utils.confirmDialog( context, - AppState().isArabic(context) ? "هل أنت متأكد من تسجيل الخروج؟" : "Are you sure you want to log out?", + LocaleKeys.areYouSureWantToLogOut.tr(), onTap: () { ApiConsts.selectedMemberType = MemberType.employee; ApiConsts.ISDependetLogin = false; @@ -263,7 +395,82 @@ class _DashboardScreenForDependetState extends State elevation: 0, surfaceTintColor: Colors.transparent, automaticallyImplyLeading: false, - toolbarHeight: 0, + titleSpacing: 0, + title: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + InkWell( + borderRadius: BorderRadius.circular(10), + onTap: () { + if (Navigator.canPop(context)) { + Navigator.pop(context); + } else { + _performLogout(); + } + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 8), + child: Icon( + isAr ? Icons.arrow_forward_ios_rounded : Icons.arrow_back_ios_new_rounded, + size: 20, + color: const Color(0xFF1E293B), + ), + ), + ), + const SizedBox(width: 8), + SvgPicture.asset( + "assets/icons/mazaya_brand.svg", + height: 48, + fit: BoxFit.contain, + ), + ], + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (!ApiConsts.isDependetLogin) ...[ + IconButton( + icon: Image.asset( + 'assets/icons/ic_add_dependents.png', + width: 24, + height: 24, + color: const Color(0xFF1E293B), + errorBuilder: (context, error, stackTrace) => SvgPicture.asset( + 'assets/icons/ic_dependents_top.svg', + width: 24, + height: 24, + colorFilter: const ColorFilter.mode(Color(0xFF1E293B), BlendMode.srcIn), + ), + ), + tooltip: LocaleKeys.addDependents.tr(), + onPressed: () { + Navigator.pushNamed( + context, + AppRoutes.familyMembers, + arguments: {'fromMazaya': true}, + ); + }, + ), + ], + IconButton( + icon: const Icon(Icons.home_rounded, size: 26, color: Color(0xFF1E293B)), + onPressed: () { + if (ApiConsts.isDependetLogin) { + _onRefresh(); + } else { + Navigator.popUntil(context, ModalRoute.withName(AppRoutes.dashboard)); + } + }, + ), + ], + ), + ], + ), + ), ), body: SafeArea( child: RefreshIndicator( @@ -272,157 +479,28 @@ class _DashboardScreenForDependetState extends State child: SingleChildScrollView( controller: _scrollController, physics: const AlwaysScrollableScrollPhysics(parent: BouncingScrollPhysics()), - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Top Header: Welcome back & User Name & Logout - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - isAr ? "مرحبا بعودتك" : "Welcome back", - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: Color(0xFF6B7280), - ), - ), - 4.height, - Text( - _getUserDisplayName(), - style: const TextStyle( - fontSize: 22, - fontWeight: FontWeight.w700, - color: Color(0xFF111827), - letterSpacing: -0.3, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ], - ), - ), - 10.width, - // Logout button - InkWell( - borderRadius: BorderRadius.circular(10), - onTap: _performLogout, - child: Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: MyColors.redColor.withOpacity(0.08), - borderRadius: BorderRadius.circular(10), - ), - child: const Icon(Icons.logout_rounded, size: 20, color: MyColors.redColor), - ), - ), - ], - ), - 20.height, - - // Search Input Box + // Search Input Card _buildSearchBar(isAr), - 18.height, - - // Nearest Offers Checkbox - _buildNearestOffersRow(isAr), - 16.height, - - // Categories Section - Always preserved once loaded with "All" prepended - if (isCategoriesLoading && categoriesList.isEmpty) - _buildCategoriesShimmer() - else if (categoriesList.isNotEmpty) ...[ - SizedBox( - height: 42.0, - width: double.infinity, - child: ListView.separated( - scrollDirection: Axis.horizontal, - physics: const BouncingScrollPhysics(), - itemCount: categoriesList.length, - separatorBuilder: (cxt, index) => 10.width, - itemBuilder: (BuildContext cxt, int index) { - WalaPlusCategoryModel item = categoriesList[index]; - bool isSelected = currentCategoryId == item.id; - String title = item.name ?? ""; - - return InkWell( - borderRadius: BorderRadius.circular(24), - onTap: () { - if (currentCategoryId != item.id) { - setState(() { - currentCategoryId = item.id ?? "all"; - }); - fetchOffers(categoryId: currentCategoryId); - } - }, - child: AnimatedContainer( - duration: const Duration(milliseconds: 200), - padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 8), - alignment: Alignment.center, - decoration: BoxDecoration( - color: isSelected ? const Color(0xFF212C36) : Colors.white, - borderRadius: BorderRadius.circular(24), - border: Border.all( - color: isSelected ? const Color(0xFF212C36) : const Color(0xFFE8ECEF), - width: 1.2, - ), - ), - child: Text( - title, - style: TextStyle( - fontSize: 12, - fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500, - color: isSelected ? Colors.white : const Color(0xFF2C3E50), - ), - ), - ), - ); - }, - ), - ), - 22.height, - ], - // "Popular offers" Header - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text( - isAr ? "أشهر العروض" : "Popular offers", - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.w700, - color: Color(0xFF111827), - ), - ), - InkWell( - onTap: () { - fetchOffers(categoryId: currentCategoryId); - }, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 2), - child: Text( - isAr ? "عرض الكل" : "See all", - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w600, - color: Color(0xFFE53935), - ), - ), - ), - ), - ], - ), - 16.height, + // "Browse Categories" Header + _buildBrowseCategoriesHeader(isAr), + + // Categories Section + if (categoriesList.isNotEmpty) + _buildCategoriesList(isAr) + else + _buildCategoriesShimmer(), - // Content Area - Shimmer while loading retailer data + // Type Filter Chips (All, In Store, Online) + _buildTypeFilterChips(isAr), + + const SizedBox(height: 16), + + // Offers Grid if (isLoading) _buildRetailersShimmer() else if (_filteredRetailersList.isNotEmpty) ...[ @@ -431,9 +509,9 @@ class _DashboardScreenForDependetState extends State physics: const NeverScrollableScrollPhysics(), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, - childAspectRatio: 0.68, - crossAxisSpacing: 14, - mainAxisSpacing: 14, + childAspectRatio: 0.72, + crossAxisSpacing: 12, + mainAxisSpacing: 16, ), itemCount: _filteredRetailersList.length, itemBuilder: (context, index) { @@ -442,22 +520,26 @@ class _DashboardScreenForDependetState extends State ), if (isLoadingMore) ...[ 16.height, - const Center( + Center( child: Padding( - padding: EdgeInsets.symmetric(vertical: 12), - child: SizedBox( - width: 24, - height: 24, - child: CircularProgressIndicator( - strokeWidth: 2.5, - color: MyColors.textMixColor, - ), + padding: const EdgeInsets.symmetric(vertical: 12), + child: Image.asset( + "assets/images/logos/loading_mohemm_logo.gif", + height: 48.0, + width: 48.0, ), ), ), ], ] else - const SizedBox.shrink(), + Padding( + padding: const EdgeInsets.only(top: 40), + child: Center( + child: LocaleKeys.noOffersAvailable.tr().toText14( + color: const Color(0xFF64748B), + ), + ), + ), ], ), ), @@ -468,99 +550,373 @@ class _DashboardScreenForDependetState extends State Widget _buildSearchBar(bool isAr) { return Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + width: double.infinity, + height: 70, + padding: const EdgeInsets.only(top: 14, right: 16, bottom: 14, left: 16), decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(14), - border: Border.all(color: const Color(0xFFE5E7EB), width: 1.2), + color: const Color(0xFFFFFFFF), + borderRadius: BorderRadius.circular(16), + border: Border.all(color: const Color(0xFFE8E8E8), width: 1.0), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.03), - blurRadius: 10, + color: const Color(0xFF000000).withOpacity(0.0314), offset: const Offset(0, 2), + blurRadius: 5, + spreadRadius: 0, ), ], ), - child: Row( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - isAr ? "بحث" : "Search", - style: const TextStyle( - fontSize: 11, - fontWeight: FontWeight.w600, - color: Color(0xFF6B7280), - ), - ), - TextField( - controller: _searchController, - onChanged: _runFilter, - decoration: InputDecoration( - isDense: true, - contentPadding: const EdgeInsets.symmetric(vertical: 3), - hintText: isAr ? "بحث" : "Search", - hintStyle: const TextStyle( - color: Color(0xFF9CA3AF), - fontSize: 14, - ), - border: InputBorder.none, - ), - style: const TextStyle( - color: Color(0xFF111827), - fontSize: 14, - ), - ), - ], + LocaleKeys.search.tr().toText12( + isBold: true, + color: const Color(0xFF2A333C), + ), + const SizedBox(height: 2), + TextField( + controller: _searchController, + onChanged: _runFilter, + decoration: InputDecoration( + isDense: true, + contentPadding: EdgeInsets.zero, + hintText: LocaleKeys.searchItems.tr(), + hintStyle: const TextStyle( + color: Color(0xFFACAFB1), + fontSize: 14, + fontWeight: FontWeight.w400, + height: 1.2, + ), + border: InputBorder.none, + ), + style: const TextStyle( + color: Color(0xFF2A333C), + fontSize: 14, + fontWeight: FontWeight.w400, ), ), - const Icon(Icons.search, color: Color(0xFF6B7280), size: 22), ], ), ); } - Widget _buildNearestOffersRow(bool isAr) { - return InkWell( - borderRadius: BorderRadius.circular(8), - onTap: () { - setState(() { - isNearestOffers = !isNearestOffers; - }); - fetchOffers(categoryId: currentCategoryId); - }, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 4), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - width: 22, - height: 22, - decoration: BoxDecoration( - color: isNearestOffers ? const Color(0xFF00A651) : Colors.white, - borderRadius: BorderRadius.circular(6), - border: Border.all( - color: isNearestOffers ? const Color(0xFF00A651) : const Color(0xFFD1D5DB), - width: 1.5, - ), + Widget _buildBrowseCategoriesHeader(bool isAr) { + return Padding( + padding: const EdgeInsets.only(top: 22, bottom: 14), + child: LocaleKeys.browseCategories.tr().toText18( + isBold: true, + color: const Color(0xFF1E293B), + ), + ); + } + + Widget _buildCategoriesList(bool isAr) { + return SizedBox( + height: 136.0, + width: double.infinity, + child: ListView.separated( + scrollDirection: Axis.horizontal, + physics: const BouncingScrollPhysics(), + padding: EdgeInsets.zero, + itemCount: categoriesList.length, + separatorBuilder: (_, __) => const SizedBox(width: 14), + itemBuilder: (BuildContext context, int index) { + WalaPlusCategoryModel item = categoriesList[index]; + bool isSelected = (item.id == "all" && (currentCategoryId == "all" || currentCategoryId.isEmpty)) || + (currentCategoryId.isNotEmpty && currentCategoryId == item.id); + String title = (item.id == "all") ? LocaleKeys.allCategories.tr() : (item.name ?? ""); + + return InkWell( + borderRadius: BorderRadius.circular(18), + onTap: () { + if (currentCategoryId != item.id) { + setState(() { + currentCategoryId = item.id ?? ""; + }); + fetchOffers(categoryId: currentCategoryId); + } + }, + child: SizedBox( + width: 84, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + _buildCategoryCard(item, isSelected), + const SizedBox(height: 8), + title.toText13( + // isBold: true, + color: const Color(0xFF1E293B), + maxlines: 2, + overflow: TextOverflow.ellipsis, + ), + ], ), - child: isNearestOffers ? const Icon(Icons.check, size: 16, color: Colors.white) : null, ), - 10.width, - Text( - isAr ? "العروض الأقرب" : "Nearest offers", - style: const TextStyle( - fontSize: 15, - fontWeight: FontWeight.w600, - color: Color(0xFF1F2937), + ); + }, + ), + ); + } + + Widget _buildTypeFilterChips(bool isAr) { + List types = _availableTypeFilters; + + return Padding( + padding: const EdgeInsets.only(top: 14, bottom: 2), + child: SizedBox( + height: 38, + child: ListView.separated( + scrollDirection: Axis.horizontal, + physics: const BouncingScrollPhysics(), + itemCount: types.length, + separatorBuilder: (BuildContext _, int __) => const SizedBox(width: 8), + itemBuilder: (BuildContext context, int index) { + String typeKey = types[index]; + bool isSelected = selectedTypeFilter == typeKey; + String title = _getTypeFilterTitle(typeKey); + IconData icon = _getTypeFilterIcon(typeKey); + + return InkWell( + borderRadius: BorderRadius.circular(6), + onTap: () { + if (selectedTypeFilter != typeKey) { + setState(() { + selectedTypeFilter = typeKey; + }); + _runFilter(); + } + }, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(6), + color: isSelected ? MyColors.darkIconColor : MyColors.lightGreyEAColor, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + icon, + size: 14, + color: isSelected ? MyColors.white : MyColors.black, + ), + const SizedBox(width: 5), + title.toText12( + color: isSelected ? MyColors.white : MyColors.black, + ), + ], + ), + ), + ); + }, + ), + ), + ); + } + + Widget _buildCategoryCard(WalaPlusCategoryModel item, bool isSelected) { + const double cardSize = 84.0; + const double borderRadiusVal = 18.0; + const double borderWidthVal = 2.8; + const Color activeBorderColor = Color(0xFF00A86B); + + bool isAll = (item.id == "all" || + item.id == "0" || + (item.name ?? "").toLowerCase() == "all" || + item.name == "الجميع" || + item.name == "الكل"); + + Widget imageContent; + + if (isAll) { + imageContent = Image.asset( + 'assets/images/cat_all.png', + width: double.infinity, + height: double.infinity, + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => _getFallbackCategoryAsset(item), + ); + } else { + String categoryImageUrl = item.categoryImageUrl.isNotEmpty + ? item.categoryImageUrl + : (item.image?.trim() ?? ""); + + if (categoryImageUrl.isNotEmpty) { + if (categoryImageUrl.toLowerCase().endsWith('.svg') || item.isSvg) { + imageContent = SvgPicture.network( + categoryImageUrl, + width: double.infinity, + height: double.infinity, + fit: BoxFit.cover, + placeholderBuilder: (_) => const Center( + child: SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(strokeWidth: 2, color: Color(0xFF00A86B)), ), ), - ], + ); + } else { + imageContent = Image.network( + categoryImageUrl, + width: double.infinity, + height: double.infinity, + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => _getFallbackCategoryAsset(item), + ); + } + } else { + imageContent = _getFallbackCategoryAsset(item); + } + } + + return Container( + width: cardSize, + height: cardSize, + decoration: BoxDecoration( + color: Colors.transparent, + borderRadius: BorderRadius.circular(borderRadiusVal), + border: isSelected + ? Border.all( + color: activeBorderColor, + width: borderWidthVal, + ) + : null, + ), + child: ClipRRect( + borderRadius: BorderRadius.circular( + isSelected ? (borderRadiusVal - borderWidthVal) : borderRadiusVal, ), + child: imageContent, + ), + ); + } + + Widget _getFallbackCategoryAsset(WalaPlusCategoryModel item) { + String name = (item.name ?? "").toLowerCase(); + + if (item.id == "all" || item.id == "0" || name == "all" || name == "الكل" || name == "الجميع") { + return Image.asset( + 'assets/images/cat_all.png', + width: double.infinity, + height: double.infinity, + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => _buildFallbackCategoryIconWidget(item), + ); + } else if (name.contains("shop") || name.contains("retail") || name.contains("تسوق") || name.contains("تجزئة")) { + return Image.asset( + 'assets/images/cat_retail.png', + width: double.infinity, + height: double.infinity, + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => _buildFallbackCategoryIconWidget(item), + ); + } else if (name.contains("food") || + name.contains("restaurant") || + name.contains("مطاعم") || + name.contains("أغذية") || + name.contains("cafe") || + name.contains("مقهى")) { + return Image.asset( + 'assets/images/cat_food.png', + width: double.infinity, + height: double.infinity, + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => _buildFallbackCategoryIconWidget(item), + ); + } else if (name.contains("fitness") || + name.contains("wellness") || + name.contains("sport") || + name.contains("gym") || + name.contains("رياضة") || + name.contains("لياقة") || + name.contains("صحة")) { + return Image.asset( + 'assets/images/cat_fitness.png', + width: double.infinity, + height: double.infinity, + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => _buildFallbackCategoryIconWidget(item), + ); + } + + return _buildFallbackCategoryIconWidget(item); + } + + Widget _buildFallbackCategoryIconWidget(WalaPlusCategoryModel item) { + return Container( + color: Colors.transparent, + alignment: Alignment.center, + child: _getFallbackCategoryIcon(item, const Color(0xFF168A89)), + ); + } + + Widget _getFallbackCategoryIcon(WalaPlusCategoryModel item, Color iconColor) { + String name = (item.name ?? "").toLowerCase(); + IconData iconData = Icons.grid_view_rounded; + Color color = iconColor; + if (item.id == "all" || name == "all" || name == "الكل" || name == "الجميع") { + iconData = Icons.confirmation_number_outlined; + color = const Color(0xFFE52332); + } else if (name.contains("finance") || name.contains("بنوك") || name.contains("مالي")) { + iconData = Icons.account_balance_rounded; + } else if (name.contains("entertain") || name.contains("activities") || name.contains("أنشطة") || name.contains("ترفيه")) { + iconData = Icons.theater_comedy_rounded; + } else if (name.contains("educat") || name.contains("تعليم")) { + iconData = Icons.menu_book_rounded; + } else if (name.contains("shop") || name.contains("retail") || name.contains("تسوق") || name.contains("تجزئة")) { + iconData = Icons.shopping_bag_outlined; + } else if (name.contains("health") || name.contains("medical") || name.contains("صحة") || name.contains("طبي")) { + iconData = Icons.health_and_safety_outlined; + } else if (name.contains("beauty") || name.contains("جمال") || name.contains("عناية")) { + iconData = Icons.face_retouching_natural_rounded; + } else if (name.contains("fitness") || name.contains("sport") || name.contains("gym") || name.contains("رياضة") || name.contains("لياقة")) { + iconData = Icons.fitness_center_rounded; + } else if (name.contains("hotel") || name.contains("travel") || name.contains("فندق") || name.contains("فنادق") || name.contains("سفر")) { + iconData = Icons.hotel_rounded; + } else if (name.contains("service") || name.contains("خدمات")) { + iconData = Icons.build_circle_outlined; + } else if (name.contains("food") || name.contains("restaurant") || name.contains("مطاعم") || name.contains("أغذية")) { + iconData = Icons.restaurant_rounded; + } else if (name.contains("home") || name.contains("منزل")) { + iconData = Icons.home_rounded; + } + + return Icon( + iconData, + color: color, + size: 26, + ); + } + + String _getDiscountBadge(WalaPlusRetailerModel item) { + if (item.offerTitle != null) { + RegExp percentRegex = RegExp(r'(\d+%)'); + Match? match = percentRegex.firstMatch(item.offerTitle!); + if (match != null) { + return match.group(1)!; + } + } + if (item.name != null) { + RegExp percentRegex = RegExp(r'(\d+%)'); + Match? match = percentRegex.firstMatch(item.name!); + if (match != null) { + return match.group(1)!; + } + } + return ""; + } + + Widget _buildMerchantMIcon() { + return Container( + color: const Color(0xFFED1C24), + alignment: Alignment.center, + child: "M".toText10( + isBold: true, + color: Colors.white, ), ); } @@ -573,175 +929,268 @@ class _DashboardScreenForDependetState extends State ? item.logoThumb : item.logo; + String discountText = _getDiscountBadge(item); + + String offerSubtitle = ""; + if (discountText.isNotEmpty) { + if (item.offerTitle != null && item.offerTitle!.toLowerCase().contains("off")) { + offerSubtitle = item.offerTitle!; + } else { + offerSubtitle = LocaleKeys.offDiscount.tr(args: [discountText]); + } + } else if (item.offerTitle != null && item.offerTitle!.trim().isNotEmpty) { + offerSubtitle = item.offerTitle!.trim(); + } + + String? typeText = item.getDisplayType(); + bool isOnline = item.isOnlineStore; + String validityText = item.getDisplayValidity(); + String usageText = ""; + if (item.displayUsage.isNotEmpty) { + usageText = item.displayUsage + .replaceAll('usage:', LocaleKeys.usageLabel.tr()) + .replaceAll('Usage :', LocaleKeys.usageLabel.tr()); + } + String cityText = item.getDisplayCity(isAr: isAr); + if (cityText.trim().isEmpty) { + cityText = item.displayCity; + } + return InkWell( - borderRadius: BorderRadius.circular(16), + borderRadius: BorderRadius.circular(20), onTap: () => _openRetailer(item), child: Container( decoration: BoxDecoration( color: Colors.white, - borderRadius: BorderRadius.circular(16), - border: Border.all(color: const Color(0xFFE5E7EB), width: 1.2), + borderRadius: BorderRadius.circular(20), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.04), - blurRadius: 10, - offset: const Offset(0, 3), + color: const Color(0xFF0D1720).withOpacity(0.0392), + offset: const Offset(0, 5), + blurRadius: 18, + spreadRadius: 0, ), ], ), - clipBehavior: Clip.antiAlias, + padding: const EdgeInsets.all(10), child: Column( crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, children: [ - // Top Image with Logo Badge overlay - SizedBox( - height: 110, - width: double.infinity, - child: Stack( - clipBehavior: Clip.none, - children: [ - // Banner Image - Positioned.fill( - child: (bannerUrl != null && bannerUrl.isNotEmpty) + // Inset image with rounded corners + ClipRRect( + borderRadius: BorderRadius.circular(14), + child: SizedBox( + height: 105, + width: double.infinity, + child: Stack( + fit: StackFit.expand, + children: [ + // Banner Image + (bannerUrl != null && bannerUrl.isNotEmpty) ? Image.network( bannerUrl, fit: BoxFit.cover, errorBuilder: (_, __, ___) => Container( - color: const Color(0xFFF3F4F6), - child: const Icon(Icons.broken_image, color: Color(0xFF9CA3AF), size: 36), + color: const Color(0xFFF1F5F9), + child: const Icon(Icons.image_outlined, color: Color(0xFF94A3B8), size: 32), ), ) : Container( - color: const Color(0xFFF3F4F6), - child: const Icon(Icons.image_outlined, color: Color(0xFF9CA3AF), size: 36), + color: const Color(0xFFF1F5F9), + child: const Icon(Icons.image_outlined, color: Color(0xFF94A3B8), size: 32), ), - ), - // Brand Logo Badge centered at top - if (logoUrl != null && logoUrl.isNotEmpty) - Align( - alignment: Alignment.topCenter, + // Gradient overlay at bottom of image + Positioned( + bottom: 0, + left: 0, + right: 0, + height: 50, child: Container( - margin: const EdgeInsets.only(top: 8), - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 3), decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(6), - border: Border.all(color: const Color(0xFFE5E7EB), width: 1), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.08), - blurRadius: 4, - offset: const Offset(0, 2), - ), - ], + gradient: LinearGradient( + begin: Alignment.bottomCenter, + end: Alignment.topCenter, + colors: [ + Colors.black.withOpacity(0.75), + Colors.transparent, + ], + ), ), - child: Image.network( - logoUrl, - height: 20, - width: 40, - fit: BoxFit.contain, - errorBuilder: (_, __, ___) => const SizedBox(width: 40, height: 20), + ), + ), + + // Discount badge at top right (flush with edge, pill on start side) + if (discountText.isNotEmpty) + PositionedDirectional( + top: 8, + end: 0, + child: Container( + padding: const EdgeInsetsDirectional.only( + start: 14, + end: 10, + top: 4, + bottom: 4, + ), + decoration: const BoxDecoration( + color: Color(0xFFED1C24), + borderRadius: BorderRadiusDirectional.only( + topStart: Radius.circular(20), + bottomStart: Radius.circular(20), + ), + ), + child: discountText.toText14( + isBold: true, + color: Colors.white, + ), ), ), + + // Brand logo & name at bottom left + PositionedDirectional( + bottom: 8, + start: 8, + end: 8, + child: Row( + children: [ + Container( + width: 18, + height: 18, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(4), + ), + clipBehavior: Clip.antiAlias, + child: (logoUrl != null && logoUrl.isNotEmpty) + ? Image.network( + logoUrl, + fit: BoxFit.contain, + errorBuilder: (_, __, ___) => _buildMerchantMIcon(), + ) + : _buildMerchantMIcon(), + ), + const SizedBox(width: 5), + Expanded( + child: (item.name ?? "").toText11( + isBold: true, + maxLine: 1, + color: Colors.white, + ), + ), + ], + ), ), - ], + ], + ), ), ), - // Card Body - Padding( - padding: const EdgeInsets.fromLTRB(10, 8, 10, 8), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - // Retailer Name / Bundle Title - Text( - item.name ?? "", - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - fontSize: 13, - fontWeight: FontWeight.w700, - color: Color(0xFF1E293B), - height: 1.2, - ), - ), - const SizedBox(height: 3), + const SizedBox(height: 8), - // Offer Title / Subtitle - Text( - item.offerTitle ?? "", - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - fontSize: 11, - fontWeight: FontWeight.w400, - color: Color(0xFF64748B), - ), - ), - const SizedBox(height: 6), - - // Badges Row 1: "Offer Valid" & "Usage : X" - Wrap( - spacing: 5, - runSpacing: 4, - children: [ - Container( - padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2.5), - decoration: BoxDecoration( - color: const Color(0xFFE6F7F0), - borderRadius: BorderRadius.circular(10), - ), - child: Text( - isAr ? "العرض ساري" : "Offer Valid", - style: const TextStyle( - color: Color(0xFF00A651), - fontSize: 9.5, - fontWeight: FontWeight.w600, - ), - ), + // Retailer Name / Bundle Title + (item.name ?? "").toText13( + isBold: true, + maxlines: 1, + overflow: TextOverflow.ellipsis, + color: const Color(0xFF1E293B), + ), + if (offerSubtitle.isNotEmpty) ...[ + const SizedBox(height: 2), + offerSubtitle.toText11( + maxLine: 1, + color: const Color(0xFF64748B), + ), + ], + + // Badges Row: Type, Validity, Location & Usage + if ((typeText != null && typeText.trim().isNotEmpty) || + validityText.trim().isNotEmpty || + cityText.trim().isNotEmpty || + usageText.trim().isNotEmpty) ...[ + const SizedBox(height: 6), + Wrap( + spacing: 5, + runSpacing: 4, + children: [ + // Type Chip (Online or In Store) + if (typeText != null && typeText.trim().isNotEmpty) + Container( + padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2.5), + decoration: BoxDecoration( + color: isOnline ? const Color(0xFFEFF6FF) : const Color(0xFFF5F3FF), + borderRadius: BorderRadius.circular(10), ), - Container( - padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2.5), - decoration: BoxDecoration( - color: const Color(0xFFFEF3C7), - borderRadius: BorderRadius.circular(10), - ), - child: Text( - item.displayUsage, - style: const TextStyle( - color: Color(0xFFB45309), - fontSize: 9.5, - fontWeight: FontWeight.w600, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + isOnline ? Icons.language_rounded : Icons.storefront_rounded, + size: 10, + color: isOnline ? const Color(0xFF2563EB) : const Color(0xFF7C3AED), ), - ), + const SizedBox(width: 3), + typeText.toText10( + isBold: true, + color: isOnline ? const Color(0xFF2563EB) : const Color(0xFF7C3AED), + ), + ], ), - ], - ), - const SizedBox(height: 5), + ), + + // Validity badge + if (validityText.trim().isNotEmpty) + Container( + padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2.5), + decoration: BoxDecoration( + color: const Color(0xFFE6F7F0), + borderRadius: BorderRadius.circular(10), + ), + child: validityText.toText10( + isBold: true, + color: const Color(0xFF00A651), + ), + ), // Location Badge - Container( - padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2.5), - decoration: BoxDecoration( - color: const Color(0xFFF3F4F6), - borderRadius: BorderRadius.circular(10), + if (cityText.trim().isNotEmpty) + Container( + padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2.5), + decoration: BoxDecoration( + color: const Color(0xFFF3F4F6), + borderRadius: BorderRadius.circular(10), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon( + Icons.location_on_rounded, + size: 10, + color: Color(0xFF64748B), + ), + const SizedBox(width: 2.5), + cityText.toText10( + color: const Color(0xFF4B5563), + ), + ], + ), ), - child: Text( - item.displayCity, - style: const TextStyle( - color: Color(0xFF4B5563), - fontSize: 9.5, - fontWeight: FontWeight.w500, + + // Usage badge + if (usageText.trim().isNotEmpty) + Container( + padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2.5), + decoration: BoxDecoration( + color: const Color(0xFFFEF3C7), + borderRadius: BorderRadius.circular(10), + ), + child: usageText.toText10( + isBold: true, + color: const Color(0xFFB45309), ), ), - ), ], ), - ), + ], ], ), ), @@ -757,85 +1206,77 @@ class _DashboardScreenForDependetState extends State physics: const NeverScrollableScrollPhysics(), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, - childAspectRatio: 0.68, - crossAxisSpacing: 14, - mainAxisSpacing: 14, + childAspectRatio: 0.72, + crossAxisSpacing: 12, + mainAxisSpacing: 16, ), itemCount: 4, itemBuilder: (_, __) { return Container( decoration: BoxDecoration( color: Colors.white, - borderRadius: BorderRadius.circular(16), - border: Border.all(color: const Color(0xFFE5E7EB), width: 1.2), + borderRadius: BorderRadius.circular(20), ), - clipBehavior: Clip.antiAlias, + padding: const EdgeInsets.all(10), child: Column( crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - // Top image placeholder + children: [ Container( - height: 110, + height: 105, width: double.infinity, - color: Colors.white, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(14), + ), ), - Padding( - padding: const EdgeInsets.fromLTRB(10, 8, 10, 8), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Container( - height: 13, - width: double.infinity, - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(4), - ), - ), - const SizedBox(height: 3), - Container( - height: 11, - width: 75, - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(4), - ), + const SizedBox(height: 8), + Container( + height: 14, + width: 110, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(4), + ), + ), + const SizedBox(height: 4), + Container( + height: 12, + width: 80, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(4), + ), + ), + const SizedBox(height: 6), + Wrap( + spacing: 5, + runSpacing: 4, + children: [ + Container( + height: 16, + width: 50, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(10), ), - const SizedBox(height: 6), - Row( - children: [ - Container( - height: 16, - width: 52, - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(10), - ), - ), - const SizedBox(width: 5), - Container( - height: 16, - width: 48, - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(10), - ), - ), - ], + ), + Container( + height: 16, + width: 45, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(10), ), - const SizedBox(height: 5), - Container( - height: 16, - width: 48, - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(10), - ), + ), + Container( + height: 16, + width: 45, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(10), ), - ], - ), + ), + ], ), ], ), @@ -846,35 +1287,57 @@ class _DashboardScreenForDependetState extends State } Widget _buildCategoriesShimmer() { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - height: 42.0, - child: Shimmer.fromColors( - baseColor: const Color(0xFFE2E8F0), - highlightColor: const Color(0xFFF8FAFC), - child: ListView.separated( - shrinkWrap: true, - scrollDirection: Axis.horizontal, - physics: const NeverScrollableScrollPhysics(), - itemCount: 5, - separatorBuilder: (_, __) => 10.width, - itemBuilder: (_, index) { - return Container( - width: index == 0 ? 80 : (index % 2 == 0 ? 120 : 95), - height: 42, - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(24), + return SizedBox( + height: 136.0, + child: Shimmer.fromColors( + baseColor: const Color(0xFFE2E8F0), + highlightColor: const Color(0xFFF8FAFC), + child: ListView.separated( + shrinkWrap: true, + scrollDirection: Axis.horizontal, + physics: const NeverScrollableScrollPhysics(), + padding: EdgeInsets.zero, + itemCount: 5, + separatorBuilder: (BuildContext _, int __) => const SizedBox(width: 14), + itemBuilder: (BuildContext _, int index) { + return SizedBox( + width: 84, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 84, + height: 84, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(18), + ), ), - ); - }, - ), - ), + const SizedBox(height: 8), + Container( + width: 65, + height: 11, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(4), + ), + ), + const SizedBox(height: 4), + Container( + width: 42, + height: 11, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(4), + ), + ), + ], + ), + ); + }, ), - 22.height, - ], + ), ); } } diff --git a/lib/ui/landing/wala_plus_retailer_details_screen.dart b/lib/ui/landing/wala_plus_retailer_details_screen.dart index a4b441b..c46863a 100644 --- a/lib/ui/landing/wala_plus_retailer_details_screen.dart +++ b/lib/ui/landing/wala_plus_retailer_details_screen.dart @@ -1,14 +1,21 @@ +import 'dart:ui' as ui; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; +import 'package:flutter_html/flutter_html.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:mohem_flutter_app/api/offers_and_discounts_api_client.dart'; import 'package:mohem_flutter_app/app_state/app_state.dart'; +import 'package:mohem_flutter_app/classes/colors.dart'; import 'package:mohem_flutter_app/classes/consts.dart'; +import 'package:mohem_flutter_app/classes/utils.dart'; import 'package:mohem_flutter_app/config/routes.dart'; import 'package:mohem_flutter_app/extensions/int_extensions.dart'; +import 'package:mohem_flutter_app/extensions/string_extensions.dart'; +import 'package:mohem_flutter_app/generated/locale_keys.g.dart'; import 'package:mohem_flutter_app/models/offers_and_discounts/wala_plus_retailer_model.dart'; import 'package:mohem_flutter_app/widgets/app_bar_widget.dart'; -import 'package:shimmer/shimmer.dart'; +import 'package:mohem_flutter_app/widgets/app_in_app_webview_screen.dart'; +import 'package:mohem_flutter_app/widgets/button/default_button.dart'; import 'package:url_launcher/url_launcher.dart'; class WalaPlusRetailerDetailsScreen extends StatefulWidget { @@ -27,8 +34,11 @@ class WalaPlusRetailerDetailsScreen extends StatefulWidget { class _WalaPlusRetailerDetailsScreenState extends State { WalaPlusRetailerModel? _retailer; - bool _isLoading = false; + bool _isLoading = true; + bool _hasDetailsData = false; String? _effectiveRetailerId; + bool _hasFetched = false; + bool _isRedeeming = false; @override void initState() { @@ -40,35 +50,85 @@ class _WalaPlusRetailerDetailsScreenState extends State _fetchRetailerDetails() async { - if (_effectiveRetailerId == null) return; + if (_effectiveRetailerId == null) { + if (mounted) { + setState(() { + _isLoading = false; + }); + } + return; + } setState(() { _isLoading = true; }); try { + debugPrint("WalaPlusRetailerDetailsScreen: fetching details for retailer ID: $_effectiveRetailerId"); WalaPlusRetailerModel? result = await OffersAndDiscountsApiClient().getWalaPlusRetailerById( retailerId: _effectiveRetailerId!, context: context, ); if (result != null && mounted) { + if ((result.image == null || result.image!.isEmpty || result.image == "null") && _retailer?.image != null) { + result.image = _retailer!.image; + } + if ((result.imageThumb == null || result.imageThumb!.isEmpty || result.imageThumb == "null") && _retailer?.imageThumb != null) { + result.imageThumb = _retailer!.imageThumb; + } + if ((result.logo == null || result.logo!.isEmpty || result.logo == "null") && _retailer?.logo != null) { + result.logo = _retailer!.logo; + } + if ((result.logoThumb == null || result.logoThumb!.isEmpty || result.logoThumb == "null") && _retailer?.logoThumb != null) { + result.logoThumb = _retailer!.logoThumb; + } + if ((result.name == null || result.name!.isEmpty) && _retailer?.name != null) { + result.name = _retailer!.name; + } + if ((result.type == null || result.type!.isEmpty) && _retailer?.type != null) { + result.type = _retailer!.type; + } + if ((result.locations == null || result.locations!.isEmpty) && _retailer?.locations != null && _retailer!.locations!.isNotEmpty) { + result.locations = _retailer!.locations; + } + if ((result.offers == null || result.offers!.isEmpty) && _retailer?.offers != null && _retailer!.offers!.isNotEmpty) { + result.offers = _retailer!.offers; + } + if ((result.validity == null || result.validity!.isEmpty) && _retailer?.validity != null) { + result.validity = _retailer!.validity; + } + if ((result.offerTitle == null || result.offerTitle!.isEmpty) && _retailer?.offerTitle != null) { + result.offerTitle = _retailer!.offerTitle; + } + if (result.contact == null && _retailer?.contact != null) { + result.contact = _retailer!.contact; + } + if ((result.email == null || result.email!.isEmpty) && _retailer?.email != null) { + result.email = _retailer!.email; + } + if ((result.phone == null || result.phone!.isEmpty) && _retailer?.phone != null) { + result.phone = _retailer!.phone; + } + setState(() { _retailer = result; + _hasDetailsData = true; }); } } catch (e) { @@ -82,148 +142,278 @@ class _WalaPlusRetailerDetailsScreenState extends State _openInAppBrowser(String url) async { + String title = _retailer?.name ?? LocaleKeys.offerDetails.tr(); + + await Navigator.push( + context, + MaterialPageRoute( + builder: (BuildContext context) => AppInAppWebViewScreen( + url: url, + title: title, + ), + ), + ); + } + + void _onUseOffer() { + if (_isRedeeming) return; + + List offers = _retailer?.offers ?? []; + if (offers.length > 1) { + _showOffersBottomSheet(offers); + } else if (offers.length == 1) { + WalaPlusOfferModel singleOffer = offers.first; + String offerId = (singleOffer.id != null && singleOffer.id!.isNotEmpty) + ? singleOffer.id! + : (_retailer?.id ?? _effectiveRetailerId ?? ""); + String offerType = (singleOffer.type != null && singleOffer.type!.isNotEmpty) + ? singleOffer.type! + : (_retailer?.offerType ?? "promo"); + _redeemOffer(offerId: offerId, offerType: offerType); + } else { + String offerId = _retailer?.id ?? _effectiveRetailerId ?? ""; + if (offerId.isEmpty) { + Utils.showToast(LocaleKeys.offerIdNotAvailable.tr()); return; - } catch (e) { - debugPrint("Error launching website: $e"); } + _redeemOffer(offerId: offerId, offerType: _retailer?.offerType ?? "promo"); } + } - // If promo pin exists, display a dialog with the promo code to copy - if (_retailer?.pin != null && _retailer!.pin!.isNotEmpty) { - _showPromoCodeDialog(_retailer!.pin!); + Future _redeemOffer({required String offerId, required String offerType}) async { + if (_isRedeeming) return; + if (offerId.isEmpty) { + Utils.showToast(LocaleKeys.offerIdNotAvailable.tr()); return; } - // Otherwise show terms/usage dialog - _showTermsDialog(); + setState(() { + _isRedeeming = true; + }); + Utils.showLoading(context); + + try { + Map? res = await OffersAndDiscountsApiClient().redeemWalaPlusOffer( + offerId: offerId, + offerType: offerType, + context: context, + ); + + Utils.hideLoading(context); + + String? transactionUrl; + if (res != null) { + transactionUrl = res['Transaction_Url'] ?? + res['TransactionUrl'] ?? + res['transaction_url'] ?? + res['transactionUrl'] ?? + res['Url'] ?? + res['url']; + } + + if (transactionUrl != null && transactionUrl.trim().isNotEmpty) { + await _openInAppBrowser(transactionUrl.trim()); + } else { + String? error = res?['Error']?.toString() ?? + res?['error']?.toString() ?? + res?['ErrorMessage']?.toString() ?? + res?['message']?.toString(); + if (error != null && error.isNotEmpty && error != "null") { + Utils.showToast(error); + } else { + Utils.showToast(LocaleKeys.unableToRedeemOffer.tr()); + } + } + } catch (e) { + Utils.hideLoading(context); + debugPrint("Error redeeming offer: $e"); + Utils.showToast(LocaleKeys.errorRedeemingOffer.tr()); + } finally { + if (mounted) { + setState(() { + _isRedeeming = false; + }); + } else { + _isRedeeming = false; + } + } } - void _showPromoCodeDialog(String pin) { - bool isAr = AppState().isArabic(context); - showDialog( + void _showOffersBottomSheet(List offers) { + bool isAr = Directionality.of(context) == ui.TextDirection.rtl || AppState().isArabic(context); + + showModalBottomSheet( context: context, - builder: (cxt) => AlertDialog( - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), - title: Text( - isAr ? "كود الخصم" : "Promo Code", - style: const TextStyle(fontWeight: FontWeight.bold), - ), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - decoration: BoxDecoration( - color: const Color(0xFFF3F4F6), - borderRadius: BorderRadius.circular(10), - border: Border.all(color: const Color(0xFF20A4A8), width: 1.2), + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (BuildContext sheetContext) { + return Container( + constraints: BoxConstraints( + maxHeight: MediaQuery.of(sheetContext).size.height * 0.78, + ), + decoration: const BoxDecoration( + color: Color(0xFFF8F9FA), + borderRadius: BorderRadius.vertical(top: Radius.circular(24)), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + 12.height, + Center( + child: Container( + width: 44, + height: 4.5, + decoration: BoxDecoration( + color: const Color(0xFFCBD5E1), + borderRadius: BorderRadius.circular(2.5), + ), + ), ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - pin, - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - letterSpacing: 2, - color: Color(0xFF1E293B), + 12.height, + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LocaleKeys.availableOffers.tr().toText18( + isBold: true, + color: const Color(0xFF1E293B), + ), + 4.height, + LocaleKeys.selectOfferToRedeem.tr().toText12( + color: const Color(0xFF64748B), + ), + ], + ), ), - ), - IconButton( - icon: const Icon(Icons.copy_rounded, color: Color(0xFF20A4A8)), - onPressed: () { - Clipboard.setData(ClipboardData(text: pin)); - Navigator.pop(cxt); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(isAr ? "تم نسخ الكود بنجاح" : "Promo code copied"), - duration: const Duration(seconds: 2), - ), - ); - }, - ), - ], + IconButton( + icon: const Icon(Icons.close_rounded, color: Color(0xFF64748B), size: 24), + onPressed: () => Navigator.pop(sheetContext), + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + ), + ], + ), ), - ), - ], - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(cxt), - child: Text(isAr ? "إغلاق" : "Close", style: const TextStyle(color: Color(0xFF20A4A8))), + 14.height, + const Divider(height: 1, color: Color(0xFFF1F5F9)), + Flexible( + child: ListView.separated( + shrinkWrap: true, + physics: const BouncingScrollPhysics(), + padding: const EdgeInsets.fromLTRB(20, 16, 20, 24), + itemCount: offers.length, + separatorBuilder: (_, __) => 14.height, + itemBuilder: (BuildContext _, int index) { + WalaPlusOfferModel offer = offers[index]; + return _buildOfferBottomSheetCard(sheetContext, offer, isAr); + }, + ), + ), + ], ), - ], - ), + ); + }, ); } - void _showTermsDialog() { - bool isAr = AppState().isArabic(context); - String content = _retailer?.howToUse ?? - _retailer?.arHowToUse ?? - _retailer?.enHowToUse ?? - _retailer?.usage?.toString() ?? - (isAr ? "تطبق الشروط والأحكام الخاصة بالعرض." : "Terms and conditions apply for this offer."); + Widget _buildOfferBottomSheetCard(BuildContext sheetContext, WalaPlusOfferModel offer, bool isAr) { + String title = offer.title ?? ""; + String description = offer.description ?? ""; + String type = (offer.type != null && offer.type!.isNotEmpty) ? offer.type! : "discount"; + String? expiredAt = offer.expiredAt; - // Clean html tags if simple - content = content.replaceAll(RegExp(r'<[^>]*>|&[^;]+;'), ' ').trim(); + bool hasDifferentDescription = description.trim().isNotEmpty && description.trim() != title.trim(); - showModalBottomSheet( - context: context, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + return Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: const Color(0xff000000).withOpacity(.05), + blurRadius: 10, + offset: const Offset(0, 3), + ), + ], ), - builder: (cxt) => SafeArea( - child: Padding( - padding: const EdgeInsets.all(20.0), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - isAr ? "الشروط والأحكام" : "Terms and Conditions", - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: Color(0xFF1E293B), - ), - ), - IconButton( - icon: const Icon(Icons.close), - onPressed: () => Navigator.pop(cxt), - ), - ], - ), - const SizedBox(height: 12), - Text( - content, - style: const TextStyle(fontSize: 14, color: Color(0xFF4B5563), height: 1.5), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), + decoration: BoxDecoration( + color: const Color(0xFFE6F7F0), + borderRadius: BorderRadius.circular(8), + ), + child: type.toUpperCase().toText10( + isBold: true, + color: const Color(0xFF00A651), + ), ), - const SizedBox(height: 20), + if (expiredAt != null && expiredAt.trim().isNotEmpty && expiredAt != "null") ...[ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.event_outlined, size: 12, color: Color(0xFF64748B)), + const SizedBox(width: 4), + expiredAt.toText10( + color: const Color(0xFF64748B), + ), + ], + ), + ], ], ), - ), + 10.height, + title.toText14( + isBold: true, + color: const Color(0xFF1E293B), + maxlines: 2, + ), + if (hasDifferentDescription) ...[ + 6.height, + description.toText12( + color: const Color(0xFF64748B), + maxLine: 3, + ), + ], + DefaultButton( + LocaleKeys.useOffer.tr(), + () { + Navigator.pop(sheetContext); + String offerId = (offer.id != null && offer.id!.isNotEmpty) + ? offer.id! + : (_retailer?.id ?? _effectiveRetailerId ?? ""); + String offerType = (offer.type != null && offer.type!.isNotEmpty) + ? offer.type! + : (_retailer?.offerType ?? "promo"); + _redeemOffer(offerId: offerId, offerType: offerType); + }, + height: 42, + borderRadius: 10, + fontSize: 14, + ), + ], ), ); } @override Widget build(BuildContext context) { - bool isAr = AppState().isArabic(context); - String title = _retailer?.name ?? (isAr ? "تفاصيل العرض" : "Offer Details"); + bool isAr = Directionality.of(context) == ui.TextDirection.rtl || AppState().isArabic(context); + String title = _retailer?.name ?? LocaleKeys.offerDetails.tr(); return Scaffold( backgroundColor: const Color(0xFFF8F9FA), @@ -239,115 +429,137 @@ class _WalaPlusRetailerDetailsScreenState extends State(Color(0xFF168A89)), + ), Expanded( child: SingleChildScrollView( physics: const BouncingScrollPhysics(), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), - child: Container( - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(22), - border: Border.all(color: const Color(0xFFF1F5F9), width: 1.2), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.04), - blurRadius: 12, - offset: const Offset(0, 4), - ), - ], - ), - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // 1. Top Banner Image with rounded corners - ClipRRect( - borderRadius: BorderRadius.circular(16), - child: SizedBox( - height: 190, - width: double.infinity, - child: (_retailer?.image != null && _retailer!.image!.isNotEmpty) - ? Image.network( - _retailer!.image!, - fit: BoxFit.cover, - errorBuilder: (_, __, ___) => Container( - color: const Color(0xFFF3F4F6), - child: const Icon(Icons.broken_image, size: 48, color: Color(0xFF9CA3AF)), - ), - ) - : Container( - color: const Color(0xFFF3F4F6), - child: const Icon(Icons.image_outlined, size: 48, color: Color(0xFF9CA3AF)), - ), - ), - ), - 14.height, - - // 2. Brand Strip (Human Resources - Retailer Logo - Dr Sulaiman Al Habib) - _buildBrandStrip(isAr), - 18.height, - - // 3. Retailer Name (Centered, Bold) - Center( - child: Text( - _retailer?.name ?? "", - textAlign: TextAlign.center, - style: const TextStyle( - fontSize: 22, - fontWeight: FontWeight.bold, - color: Color(0xFF1E293B), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: const Color(0xff000000).withOpacity(.05), + blurRadius: 14, + offset: const Offset(0, 4), ), - ), - ), - 18.height, - - // 4. 3 Info Badges Row (Store Type, Validity, City) - _buildInfoBadgesRow(isAr), - 22.height, - - // 5. Description Section - Text( - isAr ? "الوصف:" : "description:", - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: Color(0xFF1E293B), - ), - ), - 8.height, - Text( - _getCleanDescription(), - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w400, - color: Color(0xFF4B5563), - height: 1.45, - ), + ], ), - 16.height, - - // 6. Terms and Conditions Link (Right aligned) - Align( - alignment: isAr ? Alignment.centerLeft : Alignment.centerRight, - child: InkWell( - onTap: _showTermsDialog, - child: Text( - isAr ? "الشروط والأحكام" : "terms and conditions", - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w600, - color: Color(0xFF20A4A8), - decoration: TextDecoration.underline, + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 1. Top Banner Image with rounded corners + ClipRRect( + borderRadius: BorderRadius.circular(16), + child: SizedBox( + height: 190, + width: double.infinity, + child: (_retailer?.displayImage != null && _retailer!.displayImage!.isNotEmpty) + ? Image.network( + _retailer!.displayImage!, + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => Container( + color: const Color(0xFFF3F4F6), + child: const Icon(Icons.broken_image, size: 48, color: Color(0xFF9CA3AF)), + ), + ) + : Container( + color: const Color(0xFFF3F4F6), + child: const Icon(Icons.image_outlined, size: 48, color: Color(0xFF9CA3AF)), + ), ), ), - ), + 14.height, + + // 2. Brand Strip (Human Resources - Retailer Logo - Dr Sulaiman Al Habib) + _buildBrandStrip(isAr), + 18.height, + + // 3. Retailer Name (Centered, Bold) & Offer Title Subtitle + Center( + child: (_retailer?.name ?? "").toText22( + isBold: true, + isCentered: true, + color: const Color(0xFF1E293B), + ), + ), + if (_retailer?.offerTitle != null && + _retailer!.offerTitle!.trim().isNotEmpty && + _retailer!.offerTitle!.trim() != _retailer!.name?.trim()) ...[ + 6.height, + Center( + child: _cleanHtml(_retailer!.offerTitle!).toText16( + isCentered: true, + color: const Color(0xFF168A89), + ), + ), + ], + 18.height, + + // 4. 3 Info Badges Row (Store Type, Validity, City) + _buildInfoBadgesRow(isAr), + 22.height, + + // 5. Description Section (parsed as HTML) + "${LocaleKeys.description.tr()}:".toText16( + isBold: true, + color: const Color(0xFF1E293B), + ), + 8.height, + Html( + data: _getHowToUseHtml(isAr), + style: { + "body": Style( + fontSize: FontSize(14.0), + color: const Color(0xFF4B5563), + ), + }, + onLinkTap: (String? url, Map attributes, _) async { + if (url != null && url.isNotEmpty) { + try { + Uri uri = Uri.parse(url); + if (!uri.hasScheme) { + uri = Uri.parse("https://$url"); + } + await launchUrl(uri, mode: LaunchMode.externalApplication); + } catch (e) { + debugPrint("Error launching url: $e"); + } + } + }, + ), + ], ), - ], - ), + ), + + // 6. Locations Section (below description) + _buildLocationsSection(isAr), + + // 7. Contact Information (Email & Phone Number) + _buildContactSection(isAr), + + 16.height, + ], ), ), ), @@ -359,24 +571,12 @@ class _WalaPlusRetailerDetailsScreenState extends State[ + // Left: Retailer Logo + if (logoUrl != null && logoUrl.isNotEmpty) + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + boxShadow: [ + BoxShadow( + color: const Color(0xff000000).withOpacity(.04), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], ), - Text( - "Human Resources", - style: TextStyle( - color: Color(0xFFED1C2B), - fontSize: 8.5, - fontWeight: FontWeight.w600, - ), + padding: const EdgeInsets.all(2), + child: Image.network( + logoUrl, + height: 36, + width: 36, + fit: BoxFit.contain, + errorBuilder: (_, __, ___) => const SizedBox(width: 36, height: 36), ), - ], - ), - - // Center: Retailer Logo - if (_retailer?.logo != null && _retailer!.logo!.isNotEmpty) - Image.network( - _retailer!.logo!, - height: 36, - width: 36, - fit: BoxFit.contain, - errorBuilder: (_, __, ___) => const SizedBox(width: 36, height: 36), ) else const SizedBox(width: 36, height: 36), - // Right: Dr Sulaiman Al Habib branding with red cross - Row( - mainAxisSize: MainAxisSize.min, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.end, - mainAxisSize: MainAxisSize.min, - children: const [ - Text( - "سليمان الحبيب", - style: TextStyle( - color: Color(0xFF4B5563), - fontSize: 9, - fontWeight: FontWeight.w700, - ), - ), - Text( - "DR SULAIMAN AL HABIB", - style: TextStyle( - color: Color(0xFF4B5563), - fontSize: 8, - fontWeight: FontWeight.w600, - ), - ), - ], - ), - const SizedBox(width: 5), - SvgPicture.asset( - "assets/icons/mazaya_brand.svg", - height: 22, - fit: BoxFit.contain, - ), - ], + // Right: Mazaya Brand Logo + SvgPicture.asset( + "assets/icons/mazaya_brand.svg", + height: 48, + fit: BoxFit.contain, ), ], ); @@ -466,19 +629,15 @@ class _WalaPlusRetailerDetailsScreenState extends State${LocaleKeys.noInstructionsAvailable.tr()}

"; + } + + return _formatHtmlInstructions(content); + } + + String _formatHtmlInstructions(String raw) { + String s = raw.trim(); + + // 1. Strip surrounding quotes (handles multiple layers e.g. "\"text\"") + while ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) { + if (s.length >= 2) { + s = s.substring(1, s.length - 1).trim(); + } else { + break; + } + } + + // 2. Unescape JSON slashes & quotes + s = s + .replaceAll(r'\/', '/') + .replaceAll(r'\"', '"') + .replaceAll(r"\'", "'"); + + // 3. Decode \uXXXX Unicode escape sequences + s = s.replaceAllMapped(RegExp(r'\\u([0-9a-fA-F]{4})'), (match) { + try { + int codeUnit = int.parse(match.group(1)!, radix: 16); + return String.fromCharCode(codeUnit); + } catch (_) { + return match.group(0)!; + } + }); + + // 4. Decode common specific unicode escapes (quotes, bullets, dashes, etc.) + s = s + .replaceAll(r'\u201c', '“') + .replaceAll(r'\u201d', '”') + .replaceAll(r'\u2019', '’') + .replaceAll(r'\u2018', '‘') + .replaceAll(r'\u00a0', ' ') + .replaceAll(r'\u2022', '•') + .replaceAll(r'\u2013', '–') + .replaceAll(r'\u2014', '—') + .replaceAll(r'\u200f', '') + .replaceAll(r'\u200e', '') + .replaceAll(r'\u061f', '؟') + .replaceAll(r'\u201', '“'); // Fallback if truncated to 3 digits + + // 5. Decode HTML entities + s = s + .replaceAll('"', '"') + .replaceAll(''', "'") + .replaceAll(''', "'") + .replaceAll(' ', ' ') + .replaceAll('&', '&'); + + // 6. Check if string contains HTML tags + bool hasHtmlTags = RegExp(r'<[a-zA-Z][^>]*>').hasMatch(s); + + if (!hasHtmlTags) { + // Plain text: format nicely into structured HTML with bold step titles and line breaks + s = s.replaceAll(r'\r\n', '\n').replaceAll(r'\n', '\n'); + + // Separate step markers + RegExp stepRegex = RegExp( + r'(\b(?:First|Second|Third|Fourth|Fifth|Sixth|Seventh|Eighth|Ninth|Tenth)\s+step\s*:?|' + r'\b(?:1st|2nd|3rd|4th|5th|6th|7th|8th|9th|10th)\s+step\s*:?|' + r'(?:الخطوة\s+(?:الأولى|الثانية|الثالثة|الرابعة|الخامسة|السادسة|السابعة|الثامنة|التاسعة|العاشرة))\s*:?)', + caseSensitive: false, + ); + + s = s.replaceAllMapped(stepRegex, (match) { + String matchText = match.group(0)!.trim(); + return '

$matchText '; + }); + + // Split remaining newlines to
+ s = s.replaceAll('\n', '
'); + + // Clean leading
tags + s = s.replaceFirst(RegExp(r'^(?:)+'), ''); + + s = '

$s

'; + } + + return s; + } + + Widget _buildLocationsSection(bool isAr) { + List locations = _retailer?.locations ?? []; + if (locations.isEmpty) { + return const SizedBox.shrink(); } - // Remove HTML tags if string contains them - return desc.replaceAll(RegExp(r'<[^>]*>|&[^;]+;'), ' ').trim(); + + int totalCount = locations.length; + List previewLocations = + totalCount <= 2 ? locations : locations.take(2).toList(); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + 18.height, + // Header + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: const Color(0xFF168A89).withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + ), + child: const Icon(Icons.storefront_rounded, size: 18, color: Color(0xFF168A89)), + ), + 8.width, + LocaleKeys.locations.tr().toText16( + isBold: true, + color: const Color(0xFF1E293B), + ), + 8.width, + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: const Color(0xFFF1F5F9), + borderRadius: BorderRadius.circular(10), + ), + child: '$totalCount'.toText11( + isBold: true, + color: const Color(0xFF64748B), + ), + ), + ], + ), + if (totalCount > 2) + InkWell( + onTap: () => _showAllLocationsBottomSheet(locations, isAr), + borderRadius: BorderRadius.circular(6), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 4), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + LocaleKeys.showAllLocations.tr(args: ['$totalCount']).toText12( + color: const Color(0xFF168A89), + isBold: true, + ), + const SizedBox(width: 2), + Icon( + isAr ? Icons.chevron_left_rounded : Icons.chevron_right_rounded, + size: 18, + color: const Color(0xFF168A89), + ), + ], + ), + ), + ), + ], + ), + 12.height, + // Preview Cards + ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + padding: EdgeInsets.zero, + itemCount: previewLocations.length, + separatorBuilder: (_, __) => 10.height, + itemBuilder: (BuildContext _, int index) { + return _buildLocationCard(previewLocations[index], isAr); + }, + ), + // If more than 2, show the banner to browse all + if (totalCount > 2) ...[ + 10.height, + InkWell( + onTap: () => _showAllLocationsBottomSheet(locations, isAr), + borderRadius: BorderRadius.circular(16), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 14), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: const Color(0xff000000).withOpacity(.05), + blurRadius: 10, + offset: const Offset(0, 3), + ), + ], + ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(9), + decoration: BoxDecoration( + color: const Color(0xFF168A89).withOpacity(0.08), + shape: BoxShape.circle, + ), + child: const Icon(Icons.map_outlined, color: Color(0xFF168A89), size: 18), + ), + 12.width, + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LocaleKeys.showAllLocations.tr(args: ['$totalCount']).toText14( + isBold: true, + color: const Color(0xFF168A89), + ), + 2.height, + LocaleKeys.browseAllBranches.tr(args: ['$totalCount']).toText11( + color: const Color(0xFF64748B), + ), + ], + ), + ), + Icon( + isAr ? Icons.arrow_back_ios_rounded : Icons.arrow_forward_ios_rounded, + size: 14, + color: const Color(0xFF168A89), + ), + ], + ), + ), + ), + ], + ], + ); } - Widget _buildLoadingSkeleton() { - return Shimmer.fromColors( - baseColor: const Color(0xFFE2E8F0), - highlightColor: const Color(0xFFF8FAFC), - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Container( - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(22), + Widget _buildLocationCard(WalaPlusRetailerLocation loc, bool isAr) { + String cityText = loc.getLocalizedCity(isAr); + if (cityText.isEmpty) { + cityText = loc.city ?? ""; + } + String addressText = loc.address ?? ""; + + String primaryTitle = addressText.isNotEmpty ? addressText : cityText; + String? secondaryText = (addressText.isNotEmpty && + cityText.isNotEmpty && + addressText.toLowerCase() != cityText.toLowerCase()) + ? cityText + : null; + + return Container( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: const Color(0xff000000).withOpacity(.05), + blurRadius: 10, + offset: const Offset(0, 3), ), - padding: const EdgeInsets.all(16), - child: Column( + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // City Pill Badge + if (cityText.isNotEmpty) + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3.5), + decoration: BoxDecoration( + color: const Color(0xFF168A89).withOpacity(0.08), + borderRadius: BorderRadius.circular(6), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.location_on, size: 12, color: Color(0xFF168A89)), + const SizedBox(width: 4), + cityText.toText11( + isBold: true, + color: const Color(0xFF168A89), + ), + ], + ), + ) + else + const SizedBox.shrink(), + + // Directions / Map action button + InkWell( + onTap: () => _openMapForLocation(loc), + borderRadius: BorderRadius.circular(8), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), + decoration: BoxDecoration( + gradient: const LinearGradient( + transform: GradientRotation(.83), + begin: Alignment.topRight, + end: Alignment.bottomLeft, + colors: [MyColors.gradiantEndColor, MyColors.gradiantStartColor], + ), + borderRadius: BorderRadius.circular(8), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.directions_outlined, color: Colors.white, size: 14), + const SizedBox(width: 4), + LocaleKeys.viewOnMap.tr().toText11( + color: Colors.white, + isBold: true, + ), + ], + ), + ), + ), + ], + ), + 10.height, + // Branch title & pin icon + Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( - height: 190, - width: double.infinity, + margin: const EdgeInsets.only(top: 2), + padding: const EdgeInsets.all(6), decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(16), + color: const Color(0xFF168A89).withOpacity(0.08), + shape: BoxShape.circle, ), + child: const Icon(Icons.store_mall_directory_rounded, size: 14, color: Color(0xFF168A89)), ), - const SizedBox(height: 16), - Center( - child: Container( - height: 24, - width: 160, - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(6), - ), + 8.width, + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + primaryTitle.toText14( + isBold: true, + color: const Color(0xFF1E293B), + maxlines: 2, + ), + if (secondaryText != null) ...[ + 2.height, + secondaryText.toText12( + color: const Color(0xFF64748B), + maxLine: 1, + ), + ], + ], ), ), - const SizedBox(height: 20), - Row( - children: List.generate( - 3, - (index) => Expanded( + ], + ), + ], + ), + ); + } + + void _showAllLocationsBottomSheet(List locations, bool isAr) { + Map cityCounts = {}; + for (var loc in locations) { + String c = loc.getLocalizedCity(isAr); + if (c.isEmpty) c = loc.city ?? ""; + if (c.trim().isNotEmpty) { + cityCounts[c.trim()] = (cityCounts[c.trim()] ?? 0) + 1; + } + } + List sortedCities = cityCounts.keys.toList() + ..sort((a, b) => (cityCounts[b] ?? 0).compareTo(cityCounts[a] ?? 0)); + + String selectedCity = "ALL"; + String searchQuery = ""; + TextEditingController searchController = TextEditingController(); + + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (BuildContext sheetContext) { + return StatefulBuilder( + builder: (BuildContext _, StateSetter setSheetState) { + List filtered = locations.where((loc) { + String c = loc.getLocalizedCity(isAr); + if (c.isEmpty) c = loc.city ?? ""; + if (selectedCity != "ALL" && c.trim() != selectedCity) { + return false; + } + if (searchQuery.isNotEmpty) { + String q = searchQuery.toLowerCase(); + String addr = (loc.address ?? "").toLowerCase(); + String cityEn = (loc.cityNameEn ?? "").toLowerCase(); + String cityAr = (loc.cityNameAr ?? "").toLowerCase(); + String genericCity = (loc.city ?? "").toLowerCase(); + if (!addr.contains(q) && !cityEn.contains(q) && !cityAr.contains(q) && !genericCity.contains(q)) { + return false; + } + } + return true; + }).toList(); + + return Container( + constraints: BoxConstraints( + maxHeight: MediaQuery.of(sheetContext).size.height * 0.85, + ), + decoration: const BoxDecoration( + color: Color(0xFFF8F9FA), + borderRadius: BorderRadius.vertical(top: Radius.circular(24)), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + 12.height, + Center( child: Container( - height: 75, - margin: EdgeInsets.symmetric(horizontal: index == 1 ? 8 : 0), + width: 44, + height: 4.5, + decoration: BoxDecoration( + color: const Color(0xFFCBD5E1), + borderRadius: BorderRadius.circular(2.5), + ), + ), + ), + 12.height, + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LocaleKeys.locations.tr().toText18( + isBold: true, + color: const Color(0xFF1E293B), + ), + 3.height, + LocaleKeys.branchesCount.tr(args: ['${locations.length}']).toText12( + color: const Color(0xFF64748B), + ), + ], + ), + ), + IconButton( + icon: const Icon(Icons.close_rounded, color: Color(0xFF64748B), size: 24), + onPressed: () => Navigator.pop(sheetContext), + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + ), + ], + ), + ), + 12.height, + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Container( + height: 44, decoration: BoxDecoration( color: Colors.white, - borderRadius: BorderRadius.circular(14), + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: const Color(0xff000000).withOpacity(.04), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + children: [ + 12.width, + const Icon(Icons.search_rounded, size: 20, color: Color(0xFF94A3B8)), + 8.width, + Expanded( + child: TextField( + controller: searchController, + style: const TextStyle(fontSize: 13, color: Color(0xFF1E293B)), + decoration: InputDecoration( + hintText: LocaleKeys.searchBranches.tr(), + hintStyle: const TextStyle(fontSize: 13, color: Color(0xFF94A3B8)), + border: InputBorder.none, + isDense: true, + contentPadding: EdgeInsets.zero, + ), + onChanged: (val) { + setSheetState(() { + searchQuery = val.trim(); + }); + }, + ), + ), + if (searchQuery.isNotEmpty) + GestureDetector( + onTap: () { + searchController.clear(); + setSheetState(() { + searchQuery = ""; + }); + }, + child: const Padding( + padding: EdgeInsets.symmetric(horizontal: 10), + child: Icon(Icons.cancel, size: 18, color: Color(0xFF94A3B8)), + ), + ), + ], ), ), ), - ), + if (sortedCities.length > 1) ...[ + 10.height, + SizedBox( + height: 38, + child: ListView.separated( + scrollDirection: Axis.horizontal, + physics: const BouncingScrollPhysics(), + padding: const EdgeInsets.symmetric(horizontal: 20), + itemCount: sortedCities.length + 1, + separatorBuilder: (_, __) => 8.width, + itemBuilder: (BuildContext _, int index) { + if (index == 0) { + bool isSelected = selectedCity == "ALL"; + return GestureDetector( + onTap: () { + setSheetState(() { + selectedCity = "ALL"; + }); + }, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(6), + color: isSelected ? MyColors.darkIconColor : MyColors.lightGreyEAColor, + ), + child: '${LocaleKeys.allCities.tr()} (${locations.length})'.toText12( + color: isSelected ? MyColors.white : MyColors.black, + ), + ), + ); + } + String cName = sortedCities[index - 1]; + int cCount = cityCounts[cName] ?? 0; + bool isSelected = selectedCity == cName; + return GestureDetector( + onTap: () { + setSheetState(() { + selectedCity = cName; + }); + }, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(6), + color: isSelected ? MyColors.darkIconColor : MyColors.lightGreyEAColor, + ), + child: '$cName ($cCount)'.toText12( + color: isSelected ? MyColors.white : MyColors.black, + ), + ), + ); + }, + ), + ), + ], + 12.height, + const Divider(height: 1, color: Color(0xFFF1F5F9)), + Flexible( + child: filtered.isEmpty + ? Center( + child: Padding( + padding: const EdgeInsets.all(32), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.location_off_outlined, size: 44, color: Color(0xFF94A3B8)), + 12.height, + LocaleKeys.noBranchesFound.tr().toText14( + isBold: true, + color: const Color(0xFF64748B), + ), + ], + ), + ), + ) + : ListView.separated( + shrinkWrap: true, + physics: const BouncingScrollPhysics(), + padding: const EdgeInsets.fromLTRB(20, 16, 20, 24), + itemCount: filtered.length, + separatorBuilder: (_, __) => 10.height, + itemBuilder: (BuildContext _, int index) { + return _buildLocationCard(filtered[index], isAr); + }, + ), + ), + ], ), - const SizedBox(height: 24), - Container( - height: 16, - width: 100, - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(4), - ), + ); + }, + ); + }, + ); + } + + Future _openMapForLocation(WalaPlusRetailerLocation loc) async { + bool hasCoordinates = loc.latitude != null && + loc.longitude != null && + loc.latitude != 0 && + loc.longitude != 0; + + Uri googleMapsUrl; + if (hasCoordinates) { + googleMapsUrl = Uri.parse("https://www.google.com/maps/search/?api=1&query=${loc.latitude},${loc.longitude}"); + } else { + String query = "${loc.address ?? ''} ${loc.city ?? ''}".trim(); + if (query.isEmpty) { + query = _retailer?.name ?? ""; + } + googleMapsUrl = Uri.parse("https://www.google.com/maps/search/?api=1&query=${Uri.encodeComponent(query)}"); + } + + try { + if (await canLaunchUrl(googleMapsUrl)) { + await launchUrl(googleMapsUrl, mode: LaunchMode.externalApplication); + } else { + await launchUrl(googleMapsUrl); + } + } catch (e) { + debugPrint("Error launching maps: $e"); + } + } + + Widget _buildContactSection(bool isAr) { + String? email = _retailer?.resolvedEmail; + String? phone = _retailer?.resolvedPhone; + + if ((email == null || email.isEmpty) && (phone == null || phone.isEmpty)) { + return const SizedBox.shrink(); + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + 18.height, + LocaleKeys.contactInformation.tr().toText16( + isBold: true, + color: const Color(0xFF1E293B), + ), + 10.height, + if (phone != null && phone.isNotEmpty) ...[ + InkWell( + onTap: () async { + try { + Uri phoneUri = Uri(scheme: 'tel', path: phone); + await launchUrl(phoneUri); + } catch (e) { + debugPrint("Error launching phone: $e"); + } + }, + borderRadius: BorderRadius.circular(14), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(14), + boxShadow: [ + BoxShadow( + color: const Color(0xff000000).withOpacity(.05), + blurRadius: 10, + offset: const Offset(0, 3), + ), + ], ), - const SizedBox(height: 10), - Container( - height: 14, - width: double.infinity, - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(4), - ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: const Color(0xFF168A89).withOpacity(0.12), + borderRadius: BorderRadius.circular(8), + ), + child: const Icon(Icons.phone_outlined, color: Color(0xFF168A89), size: 20), + ), + 12.width, + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LocaleKeys.phoneNumber.tr().toText12( + color: const Color(0xFF64748B), + ), + const SizedBox(height: 2), + phone.toText14( + color: const Color(0xFF1E293B), + ), + ], + ), + ), + Icon( + isAr ? Icons.arrow_back_ios_rounded : Icons.arrow_forward_ios_rounded, + size: 13, + color: const Color(0xFF94A3B8), + ), + ], ), - const SizedBox(height: 6), - Container( - height: 14, - width: 220, - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(4), - ), + ), + ), + if (email != null && email.isNotEmpty) 10.height, + ], + if (email != null && email.isNotEmpty) ...[ + InkWell( + onTap: () async { + try { + Uri emailUri = Uri(scheme: 'mailto', path: email); + await launchUrl(emailUri); + } catch (e) { + debugPrint("Error launching email: $e"); + } + }, + borderRadius: BorderRadius.circular(14), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(14), + boxShadow: [ + BoxShadow( + color: const Color(0xff000000).withOpacity(.05), + blurRadius: 10, + offset: const Offset(0, 3), + ), + ], ), - ], + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: const Color(0xFF168A89).withOpacity(0.12), + borderRadius: BorderRadius.circular(8), + ), + child: const Icon(Icons.email_outlined, color: Color(0xFF168A89), size: 20), + ), + 12.width, + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LocaleKeys.email.tr().toText12( + color: const Color(0xFF64748B), + ), + const SizedBox(height: 2), + email.toText14( + color: const Color(0xFF1E293B), + ), + ], + ), + ), + Icon( + isAr ? Icons.arrow_back_ios_rounded : Icons.arrow_forward_ios_rounded, + size: 13, + color: const Color(0xFF94A3B8), + ), + ], + ), + ), ), - ), - ), + ], + ], ); } + + String _cleanHtml(String text) { + String decoded = WalaPlusRetailerModel.decodeUnicodeString(text) ?? text; + return decoded + .replaceAll(RegExp(r'', caseSensitive: false), '\n') + .replaceAll(RegExp(r'

', caseSensitive: false), '\n\n') + .replaceAll(RegExp(r'<[^>]*>'), '') + .replaceAll(' ', ' ') + .replaceAll('&', '&') + .replaceAll('"', '"') + .replaceAll(''', "'") + .replaceAll('<', '<') + .replaceAll('>', '>') + .trim(); + } } diff --git a/lib/ui/login/login_screen.dart b/lib/ui/login/login_screen.dart index cf6e804..81e79c3 100644 --- a/lib/ui/login/login_screen.dart +++ b/lib/ui/login/login_screen.dart @@ -1,7 +1,7 @@ import 'dart:io'; +import 'dart:ui' as ui; import 'package:easy_localization/easy_localization.dart'; -import 'package:easy_localization/src/public_ext.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_crashlytics/firebase_crashlytics.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; @@ -12,7 +12,6 @@ import 'package:flutter/services.dart'; // import 'package:huawei_hmsavailability/huawei_hmsavailability.dart'; import 'package:mohem_flutter_app/api/login_api_client.dart'; import 'package:mohem_flutter_app/app_state/app_state.dart'; -import 'package:intl/intl.dart'; import 'package:mohem_flutter_app/classes/colors.dart'; import 'package:mohem_flutter_app/classes/consts.dart'; import 'package:mohem_flutter_app/classes/notifications.dart'; @@ -27,6 +26,7 @@ import 'package:mohem_flutter_app/models/basic_member_information_model.dart'; import 'package:mohem_flutter_app/models/check_mobile_app_version_model.dart'; import 'package:mohem_flutter_app/models/generic_response_model.dart'; import 'package:mohem_flutter_app/models/get_mobile_login_info_list_model.dart'; +import 'package:mohem_flutter_app/models/dependent/get_mobile_login_info_dependent_model.dart'; import 'package:mohem_flutter_app/models/member_information_list_model.dart'; import 'package:mohem_flutter_app/models/member_login_list_model.dart'; import 'package:mohem_flutter_app/models/privilege_list_model.dart'; @@ -53,6 +53,7 @@ class _LoginScreenState extends State { TextEditingController dobController = TextEditingController(); DateTime? selectedDob; + // ignore: unused_field CheckMobileAppVersionModel? _checkMobileAppVersion; MemberLoginListModel? _memberLoginList; @@ -76,6 +77,9 @@ class _LoginScreenState extends State { super.initState(); ApiConsts.selectedMemberType = MemberType.employee; ApiConsts.ISDependetLogin = false; + nationalId.addListener(_onCredentialsChanged); + dobController.addListener(_onCredentialsChanged); + checkDependentPrefs(); // hmsApiAvailability = HmsApiAvailability(); // checkFirebaseToken(); if (kReleaseMode) { @@ -83,6 +87,12 @@ class _LoginScreenState extends State { } } + void _onCredentialsChanged() { + if (mounted && ApiConsts.ISDependetLogin) { + setState(() {}); + } + } + void checkDeviceSafety() { try { SafeDevice.isJailBroken.then((bool value) { @@ -105,12 +115,21 @@ class _LoginScreenState extends State { @override void dispose() { + nationalId.removeListener(_onCredentialsChanged); + dobController.removeListener(_onCredentialsChanged); dobController.dispose(); super.dispose(); } String? firebaseToken; GetMobileLoginInfoListModel? loginInfo; + GetMobileLoginInfoDependentModel? dependentLoginInfo; + + bool get _hasDeviceDependentData { + if (dependentLoginInfo == null) return false; + String id = dependentLoginInfo?.nationalId ?? dependentLoginInfo?.dependentID ?? ""; + return id.trim().isNotEmpty; + } Future checkFirebaseToken() async { try { @@ -129,6 +148,10 @@ class _LoginScreenState extends State { _firebaseMessaging = FirebaseMessaging.instance; try { firebaseToken = await _firebaseMessaging.getToken(); + if (firebaseToken != null && firebaseToken!.isNotEmpty) { + AppState().setDeviceToken = firebaseToken; + Utils.saveStringFromPrefs("P_DEP_DEVICE_TOKEN", firebaseToken!); + } } catch (ex) { firebaseToken = ""; } @@ -149,6 +172,10 @@ class _LoginScreenState extends State { await Firebase.initializeApp(); _firebaseMessaging = FirebaseMessaging.instance; firebaseToken = await _firebaseMessaging.getToken(); + if (firebaseToken != null && firebaseToken!.isNotEmpty) { + AppState().setDeviceToken = firebaseToken; + Utils.saveStringFromPrefs("P_DEP_DEVICE_TOKEN", firebaseToken!); + } String? firebaseAPNSToken = await _firebaseMessaging.getAPNSToken(); print("Firebase Token: $firebaseToken"); print("Firebase APNS Token: $firebaseAPNSToken"); @@ -165,15 +192,52 @@ class _LoginScreenState extends State { void checkLoginInfo() async { try { - loginInfo = await LoginApiClient().getMobileLoginInfoNEW( - AppState().getIsHuawei ? AppState().getHuaweiPushToken : firebaseToken ?? "", - Platform.isAndroid ? "android" : "ios", - memberType: ApiConsts.selectedMemberType.value, - ); + String devToken = AppState().getIsHuawei ? AppState().getHuaweiPushToken : (firebaseToken ?? ""); + if (devToken.isEmpty && !AppState().getIsHuawei) { + if (AppState().getDeviceToken != null && AppState().getDeviceToken!.isNotEmpty) { + devToken = AppState().getDeviceToken!; + firebaseToken = devToken; + } else { + String saved = await Utils.getStringFromPrefs("P_DEP_DEVICE_TOKEN"); + if (saved.isNotEmpty) { + devToken = saved; + firebaseToken = devToken; + } else { + try { + await Firebase.initializeApp(); + String? tok = await FirebaseMessaging.instance.getToken(); + if (tok != null && tok.isNotEmpty) { + devToken = tok; + firebaseToken = tok; + AppState().setDeviceToken = tok; + Utils.saveStringFromPrefs("P_DEP_DEVICE_TOKEN", tok); + } + } catch (e) { + if (kDebugMode) print("Error fetching FCM token in checkLoginInfo: $e"); + } + } + } + } + String devType = Platform.isAndroid ? "android" : "ios"; + if (ApiConsts.selectedMemberType.isDependent) { + dependentLoginInfo = await LoginApiClient().getMobileLoginInfoNEWDependent( + devToken, + devType, + memberType: MemberType.dependent.value, + ); Utils.hideLoading(context); + if (dependentLoginInfo != null) { + _handleDependentLoginInfo(dependentLoginInfo!); + } return; } + + loginInfo = await LoginApiClient().getMobileLoginInfoNEW( + devToken, + devType, + memberType: ApiConsts.selectedMemberType.value, + ); if (loginInfo == null) { await checkPrefs(); _autoLogin = false; @@ -190,6 +254,100 @@ class _LoginScreenState extends State { } } + void _handleDependentLoginInfo(GetMobileLoginInfoDependentModel info) { + if (info.nationalId != null && info.nationalId!.isNotEmpty) { + nationalId.text = info.nationalId!; + } else if (info.dependentID != null && info.dependentID!.isNotEmpty) { + nationalId.text = info.dependentID!; + } + + String? rawDob = (info.birthDate != null && info.birthDate!.isNotEmpty) + ? info.birthDate + : ((info.dob != null && info.dob!.isNotEmpty) ? info.dob : null); + + if (rawDob != null && rawDob.isNotEmpty) { + DateTime? parsed = _parseBirthDate(rawDob); + if (parsed != null) { + selectedDob = parsed; + dobController.text = DateFormat('yyyy-MM-dd').format(parsed); + } else { + dobController.text = rawDob; + } + } + + if (info.dependentName != null && info.dependentName!.isNotEmpty) { + AppState().setDependentName = info.dependentName; + Utils.saveStringFromPrefs("P_DEPENDENT_NAME", info.dependentName!); + } + } + + DateTime? _parseBirthDate(String? dateStr) { + if (dateStr == null || dateStr.trim().isEmpty) return null; + String s = dateStr.trim(); + + // 1. Handle Microsoft WCF JSON Date format: /Date(889401600000)/ or /Date(889401600000+0300)/ + RegExp dateRegex = RegExp(r'/?Date\((-?\d+)(?:[+-]\d+)?\)/?'); + Match? match = dateRegex.firstMatch(s); + if (match != null) { + try { + int millis = int.parse(match.group(1)!); + DateTime dtUtc = DateTime.fromMillisecondsSinceEpoch(millis, isUtc: true); + return DateTime(dtUtc.year, dtUtc.month, dtUtc.day); + } catch (_) {} + } + + // 2. Handle pure epoch milliseconds string e.g. "889401600000" + if (RegExp(r'^-?\d{10,14}$').hasMatch(s)) { + try { + int millis = int.parse(s); + DateTime dtUtc = DateTime.fromMillisecondsSinceEpoch(millis, isUtc: true); + return DateTime(dtUtc.year, dtUtc.month, dtUtc.day); + } catch (_) {} + } + + // 3. Try standard ISO-8601 (yyyy-MM-dd, yyyy-MM-ddTHH:mm:ss, etc.) + DateTime? dt = DateTime.tryParse(s); + if (dt != null) return DateTime(dt.year, dt.month, dt.day); + + // 4. Try standard custom date patterns + List formats = ['yyyy-MM-dd', 'dd/MM/yyyy', 'MM/dd/yyyy', 'dd-MM-yyyy', 'yyyy/MM/dd']; + for (String fmt in formats) { + try { + DateTime parsed = DateFormat(fmt).parseStrict(s); + return DateTime(parsed.year, parsed.month, parsed.day); + } catch (_) {} + } + + return null; + } + + Future checkDependentPrefs() async { + if (nationalId.text.isEmpty) { + String savedNid = await Utils.getStringFromPrefs("P_DEP_NATIONAL_ID"); + if (savedNid.isNotEmpty) nationalId.text = savedNid; + } + if (dobController.text.isEmpty) { + String savedDob = await Utils.getStringFromPrefs("P_DEP_BIRTH_DATE"); + if (savedDob.isNotEmpty) { + DateTime? parsed = _parseBirthDate(savedDob); + if (parsed != null) { + selectedDob = parsed; + dobController.text = DateFormat('yyyy-MM-dd').format(parsed); + } else { + dobController.text = savedDob; + } + } + } else { + // If dobController already has raw /Date(...) text, parse and format it cleanly + DateTime? parsed = _parseBirthDate(dobController.text); + if (parsed != null) { + selectedDob = parsed; + dobController.text = DateFormat('yyyy-MM-dd').format(parsed); + } + } + + } + Future checkPrefs() async { String username = await Utils.getStringFromPrefs(SharedPrefsConsts.username); if (username.isNotEmpty) { @@ -294,81 +452,111 @@ class _LoginScreenState extends State { } } - void _onTabChanged(MemberType type) { + void _switchToMode(MemberType type) async { if (ApiConsts.selectedMemberType != type) { setState(() { ApiConsts.selectedMemberType = type; + ApiConsts.ISDependetLogin = (type == MemberType.dependent); }); + if (type == MemberType.dependent) { + await checkDependentPrefs(); + } + if (firebaseToken == null || firebaseToken!.isEmpty) { + String saved = await Utils.getStringFromPrefs("P_DEP_DEVICE_TOKEN"); + if (saved.isNotEmpty) { + firebaseToken = saved; + } + } checkLoginInfo(); } } - Widget _buildTabSwitcher() { - bool isEmployee = ApiConsts.selectedMemberType == MemberType.employee; - bool isAr = AppState().isArabic(context); - - return Container( - height: 46, - decoration: BoxDecoration( - color: const Color(0xffF2F4F7), - borderRadius: BorderRadius.circular(12), - ), - padding: const EdgeInsets.all(4), - child: Row( - children: [ - Expanded( - child: Container( - decoration: BoxDecoration( - color: isEmployee ? Colors.white : Colors.transparent, - borderRadius: BorderRadius.circular(9), - boxShadow: isEmployee - ? [ - BoxShadow( - color: Colors.black.withOpacity(0.06), - blurRadius: 4, - offset: const Offset(0, 2), - ) - ] - : null, + Widget _buildCtaCard({ + required BuildContext context, + required IconData icon, + required String title, + required String subtitle, + required VoidCallback onTap, + }) { + bool isRtl = Directionality.of(context) == ui.TextDirection.rtl || AppState().isArabic(context); + + return InkWell( + borderRadius: BorderRadius.circular(14), + onTap: onTap, + child: Container( + width: double.infinity, + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: const Color(0xFFEAF8F4), + borderRadius: BorderRadius.circular(14), + border: Border.all(color: const Color(0xFFB8E8DB), width: 1.0), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + width: 40, + height: 40, + decoration: const BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, ), alignment: Alignment.center, - child: (isAr ? "الموظف" : "Employee").toText14( - color: isEmployee ? MyColors.textMixColor : MyColors.grey70Color, - isBold: isEmployee, - ), - ).onPress(() { - _onTabChanged(MemberType.employee); - }), - ), - Expanded( - child: Container( - decoration: BoxDecoration( - color: !isEmployee ? Colors.white : Colors.transparent, - borderRadius: BorderRadius.circular(9), - boxShadow: !isEmployee - ? [ - BoxShadow( - color: Colors.black.withOpacity(0.06), - blurRadius: 4, - offset: const Offset(0, 2), - ) - ] - : null, + child: Icon( + icon, + color: const Color(0xFF168A89), + size: 22, ), - alignment: Alignment.center, - child: (isAr ? "التابع" : "Dependent").toText14( - color: !isEmployee ? MyColors.textMixColor : MyColors.grey70Color, - isBold: !isEmployee, + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + title.toText14( + isBold: true, + color: const Color(0xFF1E293B), + textAlign: isRtl ? TextAlign.right : TextAlign.left, + ), + const SizedBox(height: 4), + subtitle.toText12( + color: const Color(0xFF64748B), + ), + ], ), - ).onPress(() { - _onTabChanged(MemberType.dependent); - }), - ), - ], + ), + const SizedBox(width: 8), + Icon( + isRtl ? Icons.chevron_left_rounded : Icons.chevron_right_rounded, + color: const Color(0xFF64748B), + size: 20, + ), + ], + ), ), ); } + Widget _buildDependentCtaCard(BuildContext context) { + return _buildCtaCard( + context: context, + icon: Icons.person_outline_rounded, + title: LocaleKeys.joinMazaya.tr(), + subtitle: LocaleKeys.joinMazayaSubtitle.tr(), + onTap: () => _switchToMode(MemberType.dependent), + ); + } + + Widget _buildEmployeeCtaCard(BuildContext context) { + return _buildCtaCard( + context: context, + icon: Icons.badge_outlined, + title: LocaleKeys.continueAsEmployee.tr(), + subtitle: LocaleKeys.continueAsEmployeeSubtitle.tr(), + onTap: () => _switchToMode(MemberType.employee), + ); + } + Future _selectDateOfBirth(BuildContext context) async { DateTime initialDate = selectedDob ?? DateTime(2000, 1, 1); DateTime firstDate = DateTime(1920, 1, 1); @@ -402,37 +590,7 @@ class _LoginScreenState extends State { } String getFormattedBirthDate() { - DateTime? date = selectedDob; - if (date == null && dobController.text.trim().isNotEmpty) { - String text = dobController.text.trim(); - try { - if (text.contains('-')) { - List parts = text.split('-'); - if (parts[0].length == 4) { - // yyyy-MM-dd - date = DateTime.utc(int.parse(parts[0]), int.parse(parts[1]), int.parse(parts[2])); - } else if (parts[2].length == 4) { - // dd-MM-yyyy - date = DateTime.utc(int.parse(parts[2]), int.parse(parts[1]), int.parse(parts[0])); - } - } else if (text.contains('/')) { - List parts = text.split('/'); - if (parts[0].length == 4) { - // yyyy/MM/dd - date = DateTime.utc(int.parse(parts[0]), int.parse(parts[1]), int.parse(parts[2])); - } else if (parts[2].length == 4) { - // dd/MM/yyyy - date = DateTime.utc(int.parse(parts[2]), int.parse(parts[1]), int.parse(parts[0])); - } - } else { - DateTime parsed = DateTime.parse(text); - date = DateTime.utc(parsed.year, parsed.month, parsed.day); - } - } catch (_) { - return ""; - } - } - + DateTime? date = selectedDob ?? _parseBirthDate(dobController.text.trim()); if (date != null) { int utcMillis = DateTime.utc(date.year, date.month, date.day).millisecondsSinceEpoch; return "/Date($utcMillis)/"; @@ -442,20 +600,35 @@ class _LoginScreenState extends State { void performDependentLogin() async { if (nationalId.text.trim().isEmpty) { - Utils.showToast(AppState().isArabic(context) ? "الرجاء إدخال رقم الهوية الوطنية" : "Please enter National ID"); + Utils.showToast(LocaleKeys.pleaseEnterNationalId.tr()); return; } String birthDateStr = getFormattedBirthDate(); if (birthDateStr.isEmpty) { - Utils.showToast(AppState().isArabic(context) ? "الرجاء تحديد تاريخ الميلاد" : "Please select Date of Birth"); + Utils.showToast(LocaleKeys.pleaseSelectDob.tr()); return; } Utils.showLoading(context); try { + String devToken = AppState().getIsHuawei ? AppState().getHuaweiPushToken : (firebaseToken ?? ""); + if (devToken.isEmpty && !AppState().getIsHuawei) { + if (AppState().getDeviceToken != null && AppState().getDeviceToken!.isNotEmpty) { + devToken = AppState().getDeviceToken!; + } else { + String saved = await Utils.getStringFromPrefs("P_DEP_DEVICE_TOKEN"); + if (saved.isNotEmpty) { + devToken = saved; + } + } + } + String devType = Platform.isAndroid ? "android" : "ios"; + GenericResponseModel? depLoginResponse = await LoginApiClient().memberLoginDependent( nationalId: nationalId.text.trim(), birthDate: birthDateStr, + deviceToken: devToken, + deviceType: devType, context: context, ); @@ -465,16 +638,44 @@ class _LoginScreenState extends State { return; } + if (depLoginResponse?.getWalaplusDepInfoModel != null) { + AppState().setWalaplusDepInfoModel = depLoginResponse!.getWalaplusDepInfoModel; + String? depName = depLoginResponse.getWalaplusDepInfoModel?.pDependentName; + if (depName != null && depName.trim().isNotEmpty) { + AppState().setDependentName = depName.trim(); + Utils.saveStringFromPrefs("P_DEPENDENT_NAME", depName.trim()); + } + } + + String? authToken = (depLoginResponse?.authenticationTokenID != null && + depLoginResponse!.authenticationTokenID!.trim().isNotEmpty) + ? depLoginResponse.authenticationTokenID!.trim() + : null; + String? logInTokenID = depLoginResponse?.logInTokenID; String? mobileNumber = depLoginResponse?.getWalaplusDepInfoModel?.pEmpMobileNumber; - if (logInTokenID == null || mobileNumber == null || mobileNumber.isEmpty) { + // If AuthenticationTokenID is present in MemberLogin_Dependent response: + // No need to show OTP screen, direct move to dashboard and use AuthenticationTokenID as TokenID for all other requests + if (authToken != null && authToken.isNotEmpty) { + Utils.hideLoading(context); + _completeDependentLogin( + depLoginResponse, + logInTokenID ?? authToken, + mobileNumber ?? "", + loginType: 1, + navigateToDashboard: true, + ); + return; + } + + // Step 2: Send OTP if no previous login found on this device + if (logInTokenID == null || logInTokenID.isEmpty || mobileNumber == null || mobileNumber.isEmpty) { Utils.hideLoading(context); - Utils.showToast(AppState().isArabic(context) ? "تعذر الحصول على معلومات التابع" : "Unable to retrieve dependent information"); + Utils.showToast(LocaleKeys.unableToRetrieveDependentInfo.tr()); return; } - // Step 2: Send OTP GenericResponseModel? sendOtpResponse = await LoginApiClient().mohemmSendOTPDependent( nationalId: nationalId.text.trim(), mobileNumber: mobileNumber, @@ -488,10 +689,10 @@ class _LoginScreenState extends State { // Step 3: Open OTP Dialog _showDependentOtpDialog(logInTokenID, mobileNumber); } else { - Utils.showToast(sendOtpResponse?.errorMessage ?? (AppState().isArabic(context) ? "فشل إرسال رمز التحقق" : "Failed to send OTP")); + Utils.showToast(sendOtpResponse?.errorMessage ?? LocaleKeys.failedToSendOtp.tr()); } } catch (ex) { - print(ex); + if (kDebugMode) print(ex); Utils.hideLoading(context); Utils.handleException(ex, context, null); } @@ -522,31 +723,16 @@ class _LoginScreenState extends State { } Navigator.pop(context); - String effectiveToken = (checkOtpResponse?.tokenID != null && checkOtpResponse!.tokenID!.isNotEmpty) - ? checkOtpResponse.tokenID! - : ((checkOtpResponse?.logInTokenID != null && checkOtpResponse!.logInTokenID!.isNotEmpty) - ? checkOtpResponse.logInTokenID! - : logInTokenID); - - AppState().postParamsObject?.setTokenID = effectiveToken; - AppState().postParamsObject?.setLogInTokenID = (checkOtpResponse?.logInTokenID != null && checkOtpResponse!.logInTokenID!.isNotEmpty) - ? checkOtpResponse.logInTokenID! - : logInTokenID; - AppState().postParamsObject?.setUserName = nationalId.text.trim(); - AppState().postParamsObject?.setPUserName = nationalId.text.trim(); - AppState().postParamsObject?.setMobileNumer = mobileNumber; - AppState().postParamsObject?.channel = 31; - AppState().postParamsObject?.versionID = 20; - AppState().setUserName = nationalId.text.trim(); - AppState().isAuthenticated = true; - AppState().setLogged = true; - - Utils.showToast(AppState().isArabic(context) ? "تم التحقق بنجاح" : "Verified successfully"); - if (ApiConsts.ISDependetLogin) { - Navigator.pushNamedAndRemoveUntil(context, AppRoutes.dashboardScreenForDependet, (route) => false); - } + + _completeDependentLogin( + checkOtpResponse, + logInTokenID, + mobileNumber, + loginType: 1, + navigateToDashboard: true, + ); } catch (ex) { - print(ex); + if (kDebugMode) print(ex); _pinPutController.clear(); otpFieldClear.value = ""; Utils.hideLoading(context); @@ -566,7 +752,7 @@ class _LoginScreenState extends State { context: context, ); Utils.hideLoading(context); - Utils.showToast(AppState().isArabic(context) ? "تم إعادة إرسال رمز التحقق" : "Verification code resent"); + Utils.showToast(LocaleKeys.verificationCodeResent.tr()); } catch (ex) { Utils.hideLoading(context); Utils.handleException(ex, context, null); @@ -575,6 +761,65 @@ class _LoginScreenState extends State { ).displayDialog(context); } + void _completeDependentLogin(GenericResponseModel? checkOtpResponse, String logInTokenID, String mobileNumber, {int loginType = 1, bool navigateToDashboard = true}) { + String effectiveToken = (checkOtpResponse?.authenticationTokenID != null && checkOtpResponse!.authenticationTokenID!.trim().isNotEmpty) + ? checkOtpResponse.authenticationTokenID!.trim() + : ((checkOtpResponse?.tokenID != null && checkOtpResponse!.tokenID!.isNotEmpty) + ? checkOtpResponse.tokenID! + : ((checkOtpResponse?.logInTokenID != null && checkOtpResponse!.logInTokenID!.isNotEmpty) + ? checkOtpResponse.logInTokenID! + : logInTokenID)); + + AppState().postParamsObject?.setTokenID = effectiveToken; + AppState().setAuthenticationTokenID = effectiveToken; + AppState().postParamsObject?.setLogInTokenID = (checkOtpResponse?.logInTokenID != null && checkOtpResponse!.logInTokenID!.isNotEmpty) + ? checkOtpResponse.logInTokenID! + : logInTokenID; + AppState().postParamsObject?.setUserName = nationalId.text.trim(); + AppState().postParamsObject?.setPUserName = nationalId.text.trim(); + + String resolvedMobile = mobileNumber.isNotEmpty + ? mobileNumber + : (checkOtpResponse?.getWalaplusDepInfoModel?.pEmpMobileNumber ?? ""); + if (resolvedMobile.isNotEmpty) { + AppState().postParamsObject?.setMobileNumer = resolvedMobile; + Utils.saveStringFromPrefs("P_DEP_MOBILE_NUMBER", resolvedMobile); + } + + AppState().postParamsObject?.channel = 31; + AppState().postParamsObject?.versionID = 20; + AppState().setUserName = nationalId.text.trim(); + AppState().isAuthenticated = true; + AppState().setLogged = true; + + if (checkOtpResponse?.getWalaplusDepInfoModel != null) { + AppState().setWalaplusDepInfoModel = checkOtpResponse!.getWalaplusDepInfoModel; + String? depName = checkOtpResponse.getWalaplusDepInfoModel?.pDependentName; + if (depName != null && depName.trim().isNotEmpty) { + AppState().setDependentName = depName.trim(); + Utils.saveStringFromPrefs("P_DEPENDENT_NAME", depName.trim()); + } + } + + AppState().setDependentLoginType = loginType; + AppState().setDependentDob = getFormattedBirthDate(); + + Utils.saveStringFromPrefs("P_DEP_NATIONAL_ID", nationalId.text.trim()); + Utils.saveStringFromPrefs("P_DEP_BIRTH_DATE", getFormattedBirthDate()); + Utils.saveStringFromPrefs("P_DEP_LOGIN_TOKEN_ID", AppState().postParamsObject?.logInTokenID ?? logInTokenID); + Utils.saveStringFromPrefs("P_DEP_TOKEN_ID", effectiveToken); + if (checkOtpResponse?.authenticationTokenID != null && checkOtpResponse!.authenticationTokenID!.trim().isNotEmpty) { + Utils.saveStringFromPrefs("P_DEP_AUTHENTICATION_TOKEN_ID", checkOtpResponse.authenticationTokenID!.trim()); + } + Utils.saveStringFromPrefs("P_DEP_DEVICE_TOKEN", AppState().getIsHuawei ? AppState().getHuaweiPushToken : firebaseToken ?? ""); + Utils.saveStringFromPrefs("P_DEP_LOGIN_TYPE", loginType.toString()); + + if (navigateToDashboard && ApiConsts.ISDependetLogin) { + Navigator.pushNamedAndRemoveUntil(context, AppRoutes.dashboardScreenForDependet, (route) => false); + } + } + + @override Widget build(BuildContext context) { if (isAppOpenBySystem == null) { @@ -607,9 +852,6 @@ class _LoginScreenState extends State { children: [ Row( children: [ - // Expanded( - // child:SizedBox(child: HmgConnectivityButton(),), - // ), Row( children: [ LocaleKeys.english.tr().toText14(color: AppState().isArabic(context) ? null : MyColors.textMixColor).onPress(() { @@ -629,83 +871,91 @@ class _LoginScreenState extends State { ], ), Expanded( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - _buildTabSwitcher(), - 20.height, - LocaleKeys.login.tr().toText24(isBold: true), - LocaleKeys.pleaseEnterLoginDetails.tr().toText16(), - 16.height, - if (ApiConsts.selectedMemberType == MemberType.employee) ...[ - InputWidget(LocaleKeys.username.tr(), "123456", username), - 12.height, - InputWidget(LocaleKeys.password.tr(), "xxxxxx", password, isTextIsPassword: true), - 9.height, - Align( - alignment: Alignment.centerRight, - child: LocaleKeys.forgotPassword.tr().toText12(isUnderLine: true, color: MyColors.textMixColor).onPress( - () { - // Navigator.pushNamed(context, AppRoutes.forgotPassword); - performForgotPassword(); - }, + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, ), - ), - 20.height, - ] else ...[ - InputWidget(AppState().isArabic(context) ? "الهوية الوطنية" : "National ID", "1234567890", nationalId, isInputTypeNum: true), - 12.height, - GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () => _selectDateOfBirth(context), - child: Stack( - alignment: AlignmentDirectional.centerEnd, - children: [ - IgnorePointer( - child: InputWidget( - LocaleKeys.dateOfBirth.tr(), - "YYYY-MM-DD", - dobController, - ), - ), - Positioned.directional( - textDirection: Directionality.of(context), - end: 16, - child: const Icon( - Icons.calendar_today_outlined, - size: 20, - color: MyColors.grey70Color, - ), - ), - ], + child: IntrinsicHeight( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + LocaleKeys.login.tr().toText24(isBold: true), + LocaleKeys.pleaseEnterLoginDetails.tr().toText16(), + 16.height, + if (ApiConsts.selectedMemberType == MemberType.employee) ...[ + InputWidget(LocaleKeys.username.tr(), "123456", username), + 12.height, + InputWidget(LocaleKeys.password.tr(), "xxxxxx", password, isTextIsPassword: true), + 9.height, + Align( + alignment: AlignmentDirectional.centerEnd, + child: LocaleKeys.forgotPassword.tr().toText12(isUnderLine: true, color: MyColors.textMixColor).onPress( + () { + performForgotPassword(); + }, + ), + ), + 20.height, + _buildDependentCtaCard(context), + ] else ...[ + InputWidget(LocaleKeys.nationalId.tr(), "1234567890", nationalId, isInputTypeNum: true), + 12.height, + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => _selectDateOfBirth(context), + child: Stack( + alignment: AlignmentDirectional.centerEnd, + children: [ + IgnorePointer( + child: InputWidget( + LocaleKeys.dateOfBirth.tr(), + "YYYY-MM-DD", + dobController, + ), + ), + Positioned.directional( + textDirection: Directionality.of(context), + end: 16, + child: const Icon( + Icons.calendar_today_outlined, + size: 20, + color: MyColors.grey70Color, + ), + ), + ], + ), + ), + 20.height, + _buildEmployeeCtaCard(context), + ], + ], + ), ), ), - 20.height, - ], - // DefaultButton( - // "Connect HMG Network", - // () async { - // SystemChannels.textInput.invokeMethod('TextInput.hide'); - // connectWithHmgNetwork(); - // }, - // ), - ], + ); + }, ), ) ], ), ), ), - DefaultButton(LocaleKeys.login.tr(), () async { - SystemChannels.textInput.invokeMethod('TextInput.hide'); - if (ApiConsts.ISDependetLogin) { - performDependentLogin(); - } else { - performLogin(); - } - }).insideContainer + DefaultButton( + LocaleKeys.login.tr(), + () async { + SystemChannels.textInput.invokeMethod('TextInput.hide'); + if (ApiConsts.ISDependetLogin) { + performDependentLogin(); + } else { + performLogin(); + } + }, + ).insideContainer ], ), ); diff --git a/lib/ui/my_attendance/dynamic_screens/dynamic_input_screen.dart b/lib/ui/my_attendance/dynamic_screens/dynamic_input_screen.dart index 00453b2..3af7db5 100644 --- a/lib/ui/my_attendance/dynamic_screens/dynamic_input_screen.dart +++ b/lib/ui/my_attendance/dynamic_screens/dynamic_input_screen.dart @@ -52,7 +52,7 @@ class _DynamicInputScreenState extends State { genericResponseModel = await MyAttendanceApiClient().getEitDffStructure(dynamicParams!.dynamicId, dynamicParams!.selectedEmp); getEitDffStructureList = genericResponseModel?.getEITDFFStructureList ?? []; } else { - genericResponseModel = await MyAttendanceApiClient().getCEIDffStructure(dynamicParams!.dynamicId, null); + genericResponseModel = await MyAttendanceApiClient().getCEIDffStructure(dynamicParams!.dynamicId, null, dynamicParams?.pContactRelationshipId); //added pContactRelationshipId this based on the ERP requested. getEitDffStructureList = genericResponseModel?.getCEIDFFStructureList ?? []; } diff --git a/lib/ui/profile/add_dependent_verify_screen.dart b/lib/ui/profile/add_dependent_verify_screen.dart new file mode 100644 index 0000000..aff9369 --- /dev/null +++ b/lib/ui/profile/add_dependent_verify_screen.dart @@ -0,0 +1,1902 @@ +import 'dart:io'; + +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:mohem_flutter_app/api/leave_balance_api_client.dart'; +import 'package:mohem_flutter_app/api/profile_api_client.dart'; +import 'package:mohem_flutter_app/app_state/app_state.dart'; +import 'package:mohem_flutter_app/classes/colors.dart'; +import 'package:mohem_flutter_app/classes/consts.dart'; +import 'package:mohem_flutter_app/classes/utils.dart'; +import 'package:mohem_flutter_app/config/routes.dart'; +import 'package:mohem_flutter_app/extensions/string_extensions.dart'; +import 'package:mohem_flutter_app/generated/locale_keys.g.dart'; +import 'package:mohem_flutter_app/models/dyanmic_forms/validate_eit_transaction_model.dart'; +import 'package:mohem_flutter_app/models/get_contact_dff_structure_list.dart'; +import 'package:mohem_flutter_app/models/get_eit_dff_structure_list_model.dart'; +import 'package:mohem_flutter_app/models/profile/basic_details_dff_structure.dart'; +import 'package:mohem_flutter_app/models/post_params_model.dart'; +import 'package:mohem_flutter_app/models/profile/get_contact_clos_structure_list.dart'; +import 'package:mohem_flutter_app/models/profile/get_contact_details_list.dart'; +import 'package:mohem_flutter_app/models/profile/patient_info_model.dart'; +import 'package:mohem_flutter_app/models/profile/submit_contact_transaction_list_model.dart'; +import 'package:mohem_flutter_app/ui/misc/request_submit_screen.dart'; +import 'package:mohem_flutter_app/widgets/app_bar_widget.dart'; +import 'package:mohem_flutter_app/widgets/button/default_button.dart'; +import 'package:mohem_flutter_app/widgets/dynamic_forms/dynamic_textfield_widget.dart'; + +class AddDependentVerifyScreen extends StatefulWidget { + const AddDependentVerifyScreen({Key? key}) : super(key: key); + + @override + _AddDependentVerifyScreenState createState() => _AddDependentVerifyScreenState(); +} + +class _AddDependentVerifyScreenState extends State { + PostParamsModel get _postParamsInitConfig => AppState().postParamsInitConfig; + bool get _isLegislationSA => _postParamsInitConfig.pLegislationCode?.trim().toUpperCase() == 'SA'; + + int _currentStep = 1; // 1: Verify, 2: Basic Info, 3: Additional Info, 4: Submit + bool _isLoading = false; + + // Step 1 Controllers & State + final TextEditingController _nationalIdController = TextEditingController(); + DateTime? _selectedDob; + PatientInfoModel? _patientInfo; + + // Dynamic EBS Structures + List _basicDetDffStructureList = []; + List _contactColsStructureList = []; + List _contactDffStructureList = []; + bool _ebsLoaded = false; + + // Cache for dynamically generated text field controllers + final Map _fieldControllers = {}; + + @override + void initState() { + super.initState(); + _fetchEbsStructures(); + } + + @override + void dispose() { + _nationalIdController.dispose(); + for (var c in _fieldControllers.values) { + c.dispose(); + } + _fieldControllers.clear(); + super.dispose(); + } + + TextEditingController _getControllerForField(String key, String? initialValue) { + if (!_fieldControllers.containsKey(key)) { + _fieldControllers[key] = TextEditingController(text: initialValue ?? ''); + } + return _fieldControllers[key]!; + } + + void _syncController(String key, String? value) { + if (_fieldControllers.containsKey(key)) { + _fieldControllers[key]!.text = value ?? ''; + } else { + _fieldControllers[key] = TextEditingController(text: value ?? ''); + } + } + + void _initStructureDetails() { + for (var element in _basicDetDffStructureList) { + element.getContactDetailsList ??= GetContactDetailsList(); + String colName = (element.aPPLICATIONCOLUMNNAME ?? "").toUpperCase(); + String prompt = (element.sEGMENTPROMPT ?? "").toLowerCase(); + if ((colName.contains("SPONSOR") || prompt.contains("sponsor") || prompt.contains("كفيل")) && + element.eSERVICESVS != null && element.eSERVICESVS!.isNotEmpty) { + element.getContactDetailsList!.sEGMENTVALUEDSP ??= element.eSERVICESVS!.first.vALUECOLUMNNAME; + element.getContactDetailsList!.vARCHAR2VALUE ??= element.eSERVICESVS!.first.iDCOLUMNNAME; + } + } + for (var element in _contactColsStructureList) { + element.getContactDetailsList ??= GetContactDetailsList(); + String colName = (element.aPPLICATIONCOLUMNNAME ?? "").toUpperCase(); + String prompt = (element.sEGMENTPROMPT ?? "").toLowerCase(); + if ((colName.contains("SPONSOR") || prompt.contains("sponsor") || prompt.contains("كفيل")) && + element.objectValuesList != null && element.objectValuesList!.isNotEmpty) { + element.getContactDetailsList!.sEGMENTVALUEDSP ??= element.objectValuesList!.first.mEANING; + element.getContactDetailsList!.vARCHAR2VALUE ??= element.objectValuesList!.first.cODE; + } + } + for (var element in _contactDffStructureList) { + element.getContactDetailsList ??= GetContactDetailsList(); + String colName = (element.aPPLICATIONCOLUMNNAME ?? "").toUpperCase(); + String prompt = (element.sEGMENTPROMPT ?? "").toLowerCase(); + if ((colName.contains("SPONSOR") || prompt.contains("sponsor") || prompt.contains("كفيل")) && + element.eSERVICESVS != null && element.eSERVICESVS!.isNotEmpty) { + element.getContactDetailsList!.sEGMENTVALUEDSP ??= element.eSERVICESVS!.first.vALUECOLUMNNAME; + element.getContactDetailsList!.vARCHAR2VALUE ??= element.eSERVICESVS!.first.iDCOLUMNNAME; + } + } + } + + Future _fetchEbsStructures() async { + try { + List results = await Future.wait([ + ProfileApiClient().getBasicDetDffStructure().catchError((_) => []), + ProfileApiClient().getContactColsStructureList("ADD").catchError((_) => []), + ProfileApiClient().getContactDffStructureList().catchError((_) => []), + ]); + if (mounted) { + setState(() { + _basicDetDffStructureList = results[0] as List; + _contactColsStructureList = results[1] as List; + _contactDffStructureList = results[2] as List; + _initStructureDetails(); + _ebsLoaded = true; + }); + } + } catch (_) {} + } + + // --- Step 1 Date of Birth Picker --- + Future _pickDateOfBirth(BuildContext context) async { + DateTime initial = _selectedDob ?? DateTime(DateTime.now().year - 5, 1, 1); + if (!Platform.isIOS) { + DateTime tempDate = initial; + await showCupertinoModalPopup( + context: context, + builder: (BuildContext cxt) => Container( + height: 260, + color: Colors.white, + child: Column( + children: [ + Container( + height: 44, + color: Colors.grey.shade100, + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + CupertinoButton( + padding: EdgeInsets.zero, + child: LocaleKeys.cancel.tr().toText14(color: Colors.grey), + onPressed: () => Navigator.pop(cxt), + ), + CupertinoButton( + padding: EdgeInsets.zero, + child: LocaleKeys.done + .tr() + .toText14(isBold: true, color: const Color(0xFF168A89)), + onPressed: () { + setState(() { + _selectedDob = tempDate; + }); + Navigator.pop(cxt); + }, + ), + ], + ), + ), + Expanded( + child: CupertinoDatePicker( + mode: CupertinoDatePickerMode.date, + initialDateTime: initial, + maximumDate: DateTime.now(), + minimumYear: 1920, + maximumYear: DateTime.now().year, + onDateTimeChanged: (DateTime value) { + tempDate = value; + }, + ), + ), + ], + ), + ), + ); + } else { + DateTime? picked = await showDatePicker( + context: context, + initialDate: initial, + firstDate: DateTime(1920, 1), + lastDate: DateTime.now(), + initialEntryMode: DatePickerEntryMode.calendarOnly, + ); + if (picked != null) { + setState(() { + _selectedDob = picked; + }); + } + } + } + + // --- Step 1 Action: Search User / Next --- + void _onSearchUser() async { + bool isAr = AppState().isArabic(context); + String nationalId = _nationalIdController.text.trim(); + + if (nationalId.isEmpty) { + Utils.showToast(LocaleKeys.pleaseEnterNationalId.tr()); + return; + } + + if (_isLegislationSA && nationalId.length != 10) { + Utils.showToast(LocaleKeys.nationalIdMustBe10Digits.tr()); + return; + } + + if (_selectedDob == null) { + Utils.showToast(LocaleKeys.pleaseSelectDob.tr()); + return; + } + + String formattedDob = DateFormat('dd/MM/yyyy').format(_selectedDob!); + String ymdDob = DateFormat('yyyy-MM-dd').format(_selectedDob!); + + // If legislation code is not 'SA', skip NHIC API call and prefill, proceed to manual entry + if (_postParamsInitConfig.pLegislationCode != 'SA') { + Utils.showLoading(context); + setState(() { + _isLoading = true; + }); + try { + if (!_ebsLoaded) { + await _fetchEbsStructures(); + } + _initStructureDetails(); + _syncStep1ValuesToStructures(nationalId, ymdDob); + } finally { + Utils.hideLoading(context); + setState(() { + _isLoading = false; + _currentStep = 2; // Move to Step 2 for manual entry + }); + } + return; + } + + // SA legislation flow: Call NHIC getPatientInfo API + Utils.showLoading(context); + setState(() { + _isLoading = true; + }); + + try { + PatientInfoModel? patientInfo = await ProfileApiClient().getPatientInfo( + nationalId: nationalId, + dob: formattedDob, + ); + + // Ensure EBS structures are ready + if (!_ebsLoaded) { + await _fetchEbsStructures(); + } + + Utils.hideLoading(context); + setState(() { + _isLoading = false; + }); + + if (patientInfo != null && (patientInfo.messageStatus == 1 || (patientInfo.idNumber != null && patientInfo.idNumber!.isNotEmpty))) { + _patientInfo = patientInfo; + if (_patientInfo!.dateOfBirth == null || _patientInfo!.dateOfBirth!.isEmpty) { + _patientInfo!.dateOfBirth = formattedDob; + } + + // Apply prefilled patient info across dynamic structures + _initStructureDetails(); + _applyPrefilledPatientInfo(patientInfo, isAr, nationalId, formattedDob); + + setState(() { + _currentStep = 2; // Move to Step 2: Basic Info + }); + } else { + String msg = patientInfo?.errorEndUserMessage ?? + patientInfo?.errorMessage ?? + LocaleKeys.dependentNotFoundNhic.tr(); + Utils.showToast(msg); + } + } catch (ex) { + Utils.hideLoading(context); + setState(() { + _isLoading = false; + }); + Utils.handleException(ex, context, null); + } + } + + // --- Sync Step 1 National ID & DOB for Non-SA Dynamic Structures --- + void _syncStep1ValuesToStructures(String nationalId, String? ymdDob) { + void matchStep1(colItem) { + if (colItem == null) return; + colItem.getContactDetailsList ??= GetContactDetailsList(); + GetContactDetailsList details = colItem.getContactDetailsList!; + + String colName = (colItem.aPPLICATIONCOLUMNNAME ?? '').toString().toUpperCase().trim(); + String prompt = (colItem.sEGMENTPROMPT ?? '').toString().toLowerCase().trim(); + String colKey = colItem.aPPLICATIONCOLUMNNAME ?? ''; + + // 1. Civil ID / National ID / Iqama / Identification + if (colName.contains('NATIONAL_IDENTIFIER') || + colName.contains('CIVIL_ID') || + colName.contains('IQAMA') || + colName.contains('IDENTIFIER') || + colName.contains('ID_NUMBER') || + colName.contains('ID_NO') || + colName.contains('NATIONAL_ID') || + prompt.contains('civil') || + prompt.contains('iqama') || + prompt.contains('national id') || + prompt.contains('هوية') || + prompt.contains('إقامة') || + prompt.contains('هويه')) { + details.sEGMENTVALUEDSP = nationalId; + details.vARCHAR2VALUE = nationalId; + _syncController(colKey, nationalId); + } + // 2. Date of birth + else if (colName.contains('DATE_OF_BIRTH') || + colName.contains('DOB') || + colName.contains('BIRTH') || + prompt.contains('birth') || + prompt.contains('ميلاد') || + prompt.contains('dob')) { + if (ymdDob != null && ymdDob.isNotEmpty) { + details.sEGMENTVALUEDSP = ymdDob; + details.dATEVALUE = ymdDob; + details.vARCHAR2VALUE = ymdDob; + _syncController(colKey, ymdDob); + } + } + } + + _contactColsStructureList.forEach(matchStep1); + _basicDetDffStructureList.forEach(matchStep1); + _contactDffStructureList.forEach(matchStep1); + } + + // --- Map GetPatientInfo to Dynamic Structures --- + void _applyPrefilledPatientInfo(PatientInfoModel patient, bool isAr, String nationalId, String dob) { + String fullNameAr = patient.fullNameAr; + String fullNameEn = patient.fullNameEn; + String effectiveName = isAr + ? (fullNameAr.isNotEmpty ? fullNameAr : fullNameEn) + : (fullNameEn.isNotEmpty ? fullNameEn : fullNameAr); + + DateTime? parsedDob; + if (patient.dateOfBirth != null && patient.dateOfBirth!.isNotEmpty) { + String cleanDob = patient.dateOfBirth!.replaceAll(r'\', '').trim(); + List formats = ['dd/MM/yyyy', 'MM/dd/yyyy', 'd/M/yyyy', 'M/d/yyyy', 'yyyy-MM-dd']; + for (String f in formats) { + try { + parsedDob = DateFormat(f).parseStrict(cleanDob); + break; + } catch (_) {} + } + } + if (parsedDob == null && _selectedDob != null) { + parsedDob = _selectedDob; + } + String ymdDob = parsedDob != null ? DateFormat('yyyy-MM-dd').format(parsedDob) : (patient.dateOfBirth ?? dob); + + void matchAndFill( colItem) { + if (colItem == null) return; + colItem.getContactDetailsList ??= GetContactDetailsList(); + GetContactDetailsList details = colItem.getContactDetailsList!; + + String colName = (colItem.aPPLICATIONCOLUMNNAME ?? '').toString().toUpperCase().trim(); + String prompt = (colItem.sEGMENTPROMPT ?? '').toString().toLowerCase().trim(); + String colKey = colItem.aPPLICATIONCOLUMNNAME ?? ''; + + // 1. Civil ID / National ID / Iqama + if (colName.contains('NATIONAL_IDENTIFIER') || + colName.contains('CIVIL_ID') || + colName.contains('IQAMA') || + colName.contains('IDENTIFIER') || + colName.contains('ID_NUMBER') || + colName.contains('ID_NO') || + colName.contains('NATIONAL_ID') || + prompt.contains('civil') || + prompt.contains('iqama') || + prompt.contains('national id') || + prompt.contains('هوية') || + prompt.contains('إقامة') || + prompt.contains('هويه')) { + String idVal = (patient.idNumber != null && patient.idNumber!.isNotEmpty) + ? patient.idNumber! + : nationalId; + details.sEGMENTVALUEDSP = idVal; + details.vARCHAR2VALUE = idVal; + _syncController(colKey, idVal); + } + // 2. Date of birth + else if (colName.contains('DATE_OF_BIRTH') || + colName.contains('DOB') || + colName.contains('BIRTH') || + prompt.contains('birth') || + prompt.contains('ميلاد') || + prompt.contains('dob')) { + String dobVal = ymdDob.isNotEmpty ? ymdDob : (patient.dateOfBirth ?? dob); + if (dobVal.isNotEmpty) { + details.sEGMENTVALUEDSP = dobVal; + details.dATEVALUE = dobVal; + details.vARCHAR2VALUE = dobVal; + _syncController(colKey, dobVal); + } + } + // 3. First Name + else if (colName.contains('FIRST_NAME') || + prompt.contains('first name') || + prompt.contains('الاسم الأول') || + prompt.contains('اسم الأول')) { + bool wantsAr = prompt.contains('عربي') || prompt.contains('arabic') || colName.contains('_AR') || colName.contains('ARABIC'); + String val = wantsAr + ? (patient.firstNameAr ?? patient.firstNameEn ?? '') + : (patient.firstNameEn ?? patient.firstNameAr ?? ''); + if (val.isNotEmpty) { + details.sEGMENTVALUEDSP = val; + details.vARCHAR2VALUE = val; + _syncController(colKey, val); + } + } + // 4. Second / Father Name + else if (colName.contains('SECOND_NAME') || + colName.contains('FATHER_NAME') || + prompt.contains('father') || + prompt.contains('الأب') || + prompt.contains('اسم الأب') || + prompt.contains('second name')) { + bool wantsAr = prompt.contains('عربي') || prompt.contains('arabic') || colName.contains('_AR') || colName.contains('ARABIC'); + String val = wantsAr + ? (patient.secondNameAr ?? patient.secondNameEn ?? '') + : (patient.secondNameEn ?? patient.secondNameAr ?? ''); + if (val != '-' && val.isNotEmpty) { + details.sEGMENTVALUEDSP = val; + details.vARCHAR2VALUE = val; + _syncController(colKey, val); + } + } + // 5. Third / Grandfather Name + else if (colName.contains('THIRD_NAME') || + colName.contains('GRAND_FATHER') || + colName.contains('GRAND_NAME') || + prompt.contains('grand') || + prompt.contains('الجد') || + prompt.contains('third name')) { + bool wantsAr = prompt.contains('عربي') || prompt.contains('arabic') || colName.contains('_AR') || colName.contains('ARABIC'); + String val = wantsAr + ? ((patient.thirdNameAr != '-' && patient.thirdNameAr != null) ? patient.thirdNameAr! : '') + : ((patient.thirdNameEn != '-' && patient.thirdNameEn != null) ? patient.thirdNameEn! : ''); + if (val.isNotEmpty) { + details.sEGMENTVALUEDSP = val; + details.vARCHAR2VALUE = val; + _syncController(colKey, val); + } + } + // 6. Last / Family Name + else if (colName.contains('LAST_NAME') || + colName.contains('FAMILY_NAME') || + prompt.contains('last name') || + prompt.contains('family name') || + prompt.contains('العائلة') || + prompt.contains('القبيلة') || + prompt.contains('الأخير')) { + bool wantsAr = prompt.contains('عربي') || prompt.contains('arabic') || colName.contains('_AR') || colName.contains('ARABIC'); + String val = wantsAr + ? (patient.lastNameAr ?? patient.lastNameEn ?? '') + : (patient.lastNameEn ?? patient.lastNameAr ?? ''); + if (val.isNotEmpty) { + details.sEGMENTVALUEDSP = val; + details.vARCHAR2VALUE = val; + _syncController(colKey, val); + } + } + // 7. Full Name / Contact Name + else if (colName.contains('CONTACT_NAME') || + colName.contains('FULL_NAME') || + prompt.contains('contact name') || + prompt.contains('اسم التابع') || + prompt.contains('الاسم الكامل') || + prompt.contains('اسم جهة الاتصال')) { + bool wantsAr = prompt.contains('عربي') || prompt.contains('arabic') || colName.contains('_AR') || colName.contains('ARABIC'); + String val = wantsAr ? fullNameAr : (fullNameEn.isNotEmpty ? fullNameEn : fullNameAr); + if (val.isEmpty) val = effectiveName; + if (val.isNotEmpty) { + details.sEGMENTVALUEDSP = val; + details.vARCHAR2VALUE = val; + _syncController(colKey, val); + } + } + List? objValues; + List? eServices; + if (colItem is GetContactColsStructureList) { + objValues = colItem.objectValuesList; + } else if (colItem is GetBasicDetDffStructureList) { + eServices = colItem.eSERVICESVS; + } else if (colItem is GetContactDffStructureList) { + eServices = colItem.eSERVICESVS; + } + + // 8. Gender / Sex + if (colName.contains('SEX') || + colName.contains('GENDER') || + prompt.contains('gender') || + prompt.contains('sex') || + prompt.contains('الجنس')) { + String g = (patient.gender ?? '').trim().toUpperCase(); + bool isFemale = g == 'F' || g.startsWith('FEM'); + bool isMale = g == 'M' || g.startsWith('MAL') || (!isFemale && g.isNotEmpty); + + bool matched = false; + if (objValues != null && objValues.isNotEmpty) { + // Priority 1: Match exact code ('F' or 'M') + for (var opt in objValues) { + String code = (opt.cODE ?? '').trim().toUpperCase(); + if ((isFemale && (code == 'F' || code == 'FEMALE')) || + (isMale && (code == 'M' || code == 'MALE'))) { + details.sEGMENTVALUEDSP = opt.mEANING; + details.vARCHAR2VALUE = opt.cODE; + matched = true; + break; + } + } + // Priority 2: Match meaning safely without substring collision + if (!matched) { + for (var opt in objValues) { + String meaning = (opt.mEANING ?? '').trim().toLowerCase(); + if (isFemale && (meaning == 'female' || meaning.contains('female') || meaning.contains('أنثى'))) { + details.sEGMENTVALUEDSP = opt.mEANING; + details.vARCHAR2VALUE = opt.cODE; + matched = true; + break; + } else if (isMale && !meaning.contains('female') && (meaning == 'male' || meaning.contains('male') || meaning.contains('ذكر'))) { + details.sEGMENTVALUEDSP = opt.mEANING; + details.vARCHAR2VALUE = opt.cODE; + matched = true; + break; + } + } + } + } else if (eServices != null && eServices.isNotEmpty) { + // Priority 1: Match exact ID/Code + for (var opt in eServices) { + String idCol = (opt.iDCOLUMNNAME ?? '').trim().toUpperCase(); + if ((isFemale && (idCol == 'F' || idCol == 'FEMALE')) || + (isMale && (idCol == 'M' || idCol == 'MALE'))) { + details.sEGMENTVALUEDSP = opt.vALUECOLUMNNAME; + details.vARCHAR2VALUE = opt.iDCOLUMNNAME; + matched = true; + break; + } + } + // Priority 2: Match value + if (!matched) { + for (var opt in eServices) { + String valCol = (opt.vALUECOLUMNNAME ?? '').trim().toLowerCase(); + if (isFemale && (valCol.contains('female') || valCol.contains('أنثى'))) { + details.sEGMENTVALUEDSP = opt.vALUECOLUMNNAME; + details.vARCHAR2VALUE = opt.iDCOLUMNNAME; + matched = true; + break; + } else if (isMale && !valCol.contains('female') && (valCol.contains('male') || valCol.contains('ذكر'))) { + details.sEGMENTVALUEDSP = opt.vALUECOLUMNNAME; + details.vARCHAR2VALUE = opt.iDCOLUMNNAME; + matched = true; + break; + } + } + } + } + + if (!matched) { + details.sEGMENTVALUEDSP = isFemale ? LocaleKeys.genderFemale.tr() : LocaleKeys.genderMale.tr(); + details.vARCHAR2VALUE = isFemale ? 'F' : 'M'; + } + } + // 10. Title + else if (colName.contains('TITLE') || prompt.contains('title') || prompt.contains('اللقب')) { + String g = (patient.gender ?? '').trim().toUpperCase(); + bool isFemale = g == 'F' || g.startsWith('FEM'); + bool isMale = g == 'M' || g.startsWith('MAL') || (!isFemale && g.isNotEmpty); + if (objValues != null && objValues.isNotEmpty) { + for (var opt in objValues) { + String code = (opt.cODE ?? '').trim().toUpperCase(); + String meaning = (opt.mEANING ?? '').trim().toLowerCase(); + if (isFemale && (code == 'MS' || code == 'MRS' || code == 'MISS' || meaning.contains('ms') || meaning.contains('mrs') || meaning.contains('miss') || meaning.contains('سيدة') || meaning.contains('آنسة'))) { + details.sEGMENTVALUEDSP = opt.mEANING; + details.vARCHAR2VALUE = opt.cODE; + break; + } else if (isMale && (code == 'MR' || (!meaning.contains('mrs') && !meaning.contains('سيدة') && (meaning.startsWith('mr') || meaning.contains('mr.') || meaning.contains('سيد'))))) { + details.sEGMENTVALUEDSP = opt.mEANING; + details.vARCHAR2VALUE = opt.cODE; + break; + } + } + } + } + // 11. Contact Type / Relationship + else if (colName.contains('CONTACT_TYPE') || + colName.contains('RELATIONSHIP') || + prompt.contains('relationship') || + prompt.contains('قرابة')) { + String g = (patient.gender ?? '').trim().toUpperCase(); + bool isFemale = g == 'F' || g.startsWith('FEM'); + bool isMale = g == 'M' || g.startsWith('MAL') || (!isFemale && g.isNotEmpty); + if (objValues != null && objValues.isNotEmpty) { + for (var opt in objValues) { + String code = (opt.cODE ?? '').trim().toUpperCase(); + String meaning = (opt.mEANING ?? '').trim().toLowerCase(); + if (isFemale && (code == 'DAUGHTER' || meaning.contains('daughter') || meaning.contains('ابنة') || meaning.contains('بنت'))) { + details.sEGMENTVALUEDSP = opt.mEANING; + details.vARCHAR2VALUE = opt.cODE; + break; + } else if (isMale && (code == 'SON' || (!meaning.contains('daughter') && (meaning.contains('son') || meaning.contains('ابن') || meaning.contains('ولد'))))) { + details.sEGMENTVALUEDSP = opt.mEANING; + details.vARCHAR2VALUE = opt.cODE; + break; + } + } + } + } + // 12. Religion + else if (colName.contains('RELIGION') || prompt.contains('religion') || prompt.contains('الديانة')) { + if (eServices != null && eServices.isNotEmpty) { + for (var opt in eServices) { + String valCol = (opt.vALUECOLUMNNAME ?? '').toLowerCase(); + if (valCol.contains('islam') || valCol.contains('إسلام') || valCol.contains('مسلم')) { + details.sEGMENTVALUEDSP = opt.vALUECOLUMNNAME; + details.vARCHAR2VALUE = opt.iDCOLUMNNAME; + break; + } + } + } else { + details.sEGMENTVALUEDSP = LocaleKeys.religionIslam.tr(); + details.vARCHAR2VALUE = 'ISLAM'; + } + } + // 13. Sponsor & Dependent Sponsor Detail + else if (colName.contains('SPONSOR') || prompt.contains('sponsor') || prompt.contains('كفيل')) { + bool isSponsorDetail = prompt.contains('sponsor detail') || + prompt.contains('detail') || + prompt.contains('تفاصيل') || + colName.contains('SPONSOR_DETAIL') || + colName.contains('DEP_SPONSOR') || + colName.contains('DETAIL'); + + if (isSponsorDetail) { + // Dependent Sponsor Detail: do NOT fill with name, default select first option from dropdown + if (eServices != null && eServices.isNotEmpty) { + details.sEGMENTVALUEDSP = eServices.first.vALUECOLUMNNAME; + details.vARCHAR2VALUE = eServices.first.iDCOLUMNNAME; + } else if (objValues != null && objValues.isNotEmpty) { + details.sEGMENTVALUEDSP = objValues.first.mEANING; + details.vARCHAR2VALUE = objValues.first.cODE; + } + } else { + // Sponsor field: also default select first option from dropdown + if (eServices != null && eServices.isNotEmpty) { + details.sEGMENTVALUEDSP = eServices.first.vALUECOLUMNNAME; + details.vARCHAR2VALUE = eServices.first.iDCOLUMNNAME; + } else if (objValues != null && objValues.isNotEmpty) { + details.sEGMENTVALUEDSP = objValues.first.mEANING; + details.vARCHAR2VALUE = objValues.first.cODE; + } else { + var emp = AppState().memberInformationList; + String empName = isAr + ? (emp?.eMPLOYEENAMEAr?.isNotEmpty == true ? emp!.eMPLOYEENAMEAr! : (emp?.eMPLOYEENAME ?? '')) + : (emp?.eMPLOYEENAMEEn?.isNotEmpty == true ? emp!.eMPLOYEENAMEEn! : (emp?.eMPLOYEENAME ?? '')); + if (empName.isNotEmpty) { + details.sEGMENTVALUEDSP = empName; + details.vARCHAR2VALUE = empName; + _syncController(colKey, empName); + } + } + } + } + // 14. Ticket / Education / Medical Insurance Eligibility + else if (prompt.contains('ticket') || colName.contains('TICKET') || + prompt.contains('education') || colName.contains('EDU') || + prompt.contains('insurance') || colName.contains('MED')) { + if (eServices != null && eServices.isNotEmpty) { + for (var opt in eServices) { + String valCol = (opt.vALUECOLUMNNAME ?? '').toLowerCase(); + String idCol = (opt.iDCOLUMNNAME ?? '').toUpperCase(); + if (idCol == 'Y' || valCol.contains('eligible') || valCol.contains('مؤهل') || valCol.contains('نعم') || valCol.contains('yes')) { + details.sEGMENTVALUEDSP = opt.vALUECOLUMNNAME; + details.vARCHAR2VALUE = opt.iDCOLUMNNAME; + break; + } + } + } + } + // 15. Relationship Start Date + else if (prompt.contains('start') || colName.contains('DATE_START') || colName.contains('START_DATE')) { + String startVal = ymdDob.isNotEmpty ? ymdDob : DateFormat('yyyy-MM-dd').format(DateTime.now()); + details.sEGMENTVALUEDSP = startVal; + details.dATEVALUE = startVal; + details.vARCHAR2VALUE = startVal; + } + } + + _contactColsStructureList.forEach(matchAndFill); + _basicDetDffStructureList.forEach(matchAndFill); + _contactDffStructureList.forEach(matchAndFill); + } + + // --- Dynamic Date Selector --- + Future _selectDate(BuildContext context, [DateTime? initial]) async { + DateTime time = initial ?? DateTime.now(); + if (!Platform.isIOS) { + DateTime tempDate = time; + bool? confirmed = await showCupertinoModalPopup( + context: context, + builder: (BuildContext cxt) => Container( + height: 260, + color: Colors.white, + child: Column( + children: [ + Container( + height: 44, + color: Colors.grey.shade100, + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + CupertinoButton( + padding: EdgeInsets.zero, + child: LocaleKeys.cancel.tr().toText14(color: Colors.grey), + onPressed: () => Navigator.pop(cxt, false), + ), + CupertinoButton( + padding: EdgeInsets.zero, + child: LocaleKeys.done + .tr() + .toText14(isBold: true, color: const Color(0xFF168A89)), + onPressed: () => Navigator.pop(cxt, true), + ), + ], + ), + ), + Expanded( + child: CupertinoDatePicker( + backgroundColor: Colors.white, + mode: CupertinoDatePickerMode.date, + initialDateTime: time, + minimumYear: 1920, + maximumYear: DateTime.now().year + 10, + onDateTimeChanged: (DateTime value) { + tempDate = value; + }, + ), + ), + ], + ), + ), + ); + if (confirmed == true) { + return tempDate; + } + return null; + } else { + DateTime? picked = await showDatePicker( + context: context, + initialDate: time, + firstDate: DateTime(1920, 1), + lastDate: DateTime.now().add(const Duration(days: 365 * 10)), + initialEntryMode: DatePickerEntryMode.calendarOnly, + ); + return picked; + } + } + + // --- Build Dynamic Fields for Contact Columns --- + Widget _buildDynamicColField(GetContactColsStructureList model, int index) { + if (model.dISPLAYFLAG == "N") { + return const SizedBox.shrink(); + } + + bool isRequired = model.rEQUIREDFLAG == "Y"; + String label = (model.sEGMENTPROMPT ?? "") + (isRequired ? "*" : ""); + model.getContactDetailsList ??= GetContactDetailsList(); + String colKey = model.aPPLICATIONCOLUMNNAME ?? "COL_$index"; + + if (model.dATATYPE == "VARCHAR2") { + if (model.objectValuesList != null && model.objectValuesList!.isNotEmpty) { + String colName = (model.aPPLICATIONCOLUMNNAME ?? "").toUpperCase(); + String prompt = (model.sEGMENTPROMPT ?? "").toLowerCase(); + bool isSponsorRelated = colName.contains("SPONSOR") || prompt.contains("sponsor") || prompt.contains("كفيل"); + + bool valueInOptions = model.objectValuesList!.any((opt) => opt.mEANING == model.getContactDetailsList!.sEGMENTVALUEDSP); + if (isSponsorRelated && (!valueInOptions || model.getContactDetailsList!.sEGMENTVALUEDSP == null || model.getContactDetailsList!.sEGMENTVALUEDSP!.isEmpty)) { + model.getContactDetailsList!.sEGMENTVALUEDSP = model.objectValuesList!.first.mEANING; + model.getContactDetailsList!.vARCHAR2VALUE = model.objectValuesList!.first.cODE; + } + + String displayVal = model.getContactDetailsList!.sEGMENTVALUEDSP ?? ""; + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: PopupMenuButton( + child: DynamicTextFieldWidget( + label, + displayVal.isNotEmpty ? displayVal : LocaleKeys.select.tr(), + isEnable: false, + isPopup: true, + backgroundColor: Colors.white, + ), + itemBuilder: (_) => >[ + for (int i = 0; i < model.objectValuesList!.length; i++) + PopupMenuItem( + value: i, + child: (model.objectValuesList![i].mEANING ?? "").toText14(), + ), + ], + onSelected: (int selectedIdx) { + setState(() { + model.getContactDetailsList!.sEGMENTVALUEDSP = model.objectValuesList![selectedIdx].mEANING; + model.getContactDetailsList!.vARCHAR2VALUE = model.objectValuesList![selectedIdx].cODE; + }); + }, + ), + ); + } else { + var controller = _getControllerForField(colKey, model.getContactDetailsList!.sEGMENTVALUEDSP); + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: DynamicTextFieldWidget( + label, + LocaleKeys.enterFieldPlaceholder.tr(args: [label]), + controller: controller, + backgroundColor: Colors.white, + onChange: (String text) { + model.getContactDetailsList!.sEGMENTVALUEDSP = text; + model.getContactDetailsList!.vARCHAR2VALUE = text; + }, + ), + ); + } + } else if (model.dATATYPE == "NUMBER") { + var controller = _getControllerForField(colKey, model.getContactDetailsList!.sEGMENTVALUEDSP); + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: DynamicTextFieldWidget( + label, + LocaleKeys.enterFieldPlaceholder.tr(args: [label]), + controller: controller, + isInputTypeNum: true, + backgroundColor: Colors.white, + onChange: (String text) { + model.getContactDetailsList!.sEGMENTVALUEDSP = text; + model.getContactDetailsList!.vARCHAR2VALUE = text; + }, + ), + ); + } else if (model.dATATYPE == "DATE") { + String displayVal = model.getContactDetailsList!.sEGMENTVALUEDSP ?? ""; + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: DynamicTextFieldWidget( + label, + displayVal.isNotEmpty ? displayVal : LocaleKeys.dateFormatPlaceholder.tr(), + suffixIconData: Icons.calendar_today_outlined, + isReadOnly: true, + backgroundColor: Colors.white, + onTap: () async { + DateTime initial = DateTime.now(); + if (displayVal.isNotEmpty) { + try { + initial = DateFormat('yyyy-MM-dd').parse(displayVal); + } catch (_) { + try { + initial = DateFormat('dd/MM/yyyy').parse(displayVal); + } catch (_) {} + } + } + DateTime? picked = await _selectDate(context, initial); + if (picked != null) { + String formatted = DateFormat('yyyy-MM-dd', "en_US").format(picked); + setState(() { + model.getContactDetailsList!.sEGMENTVALUEDSP = formatted; + model.getContactDetailsList!.dATEVALUE = formatted; + model.getContactDetailsList!.vARCHAR2VALUE = formatted; + }); + } + }, + ), + ); + } + return const SizedBox.shrink(); + } + + // --- Build Dynamic Fields for Basic Det DFF --- + Widget _buildDynamicBasicDffField(GetBasicDetDffStructureList model, int index) { + if (model.dISPLAYFLAG == "N") { + return const SizedBox.shrink(); + } + + bool isRequired = model.rEQUIREDFLAG == "Y"; + String label = (model.sEGMENTPROMPT ?? "") + (isRequired ? "*" : ""); + model.getContactDetailsList ??= GetContactDetailsList(); + String colKey = model.aPPLICATIONCOLUMNNAME ?? "BASIC_DFF_$index"; + + if (model.fORMATTYPE == "C") { + if (model.eSERVICESVS != null && model.eSERVICESVS!.isNotEmpty) { + String colName = (model.aPPLICATIONCOLUMNNAME ?? "").toUpperCase(); + String prompt = (model.sEGMENTPROMPT ?? "").toLowerCase(); + bool isSponsorRelated = colName.contains("SPONSOR") || prompt.contains("sponsor") || prompt.contains("كفيل"); + + bool valueInOptions = model.eSERVICESVS!.any((opt) => opt.vALUECOLUMNNAME == model.getContactDetailsList!.sEGMENTVALUEDSP); + if (isSponsorRelated && (!valueInOptions || model.getContactDetailsList!.sEGMENTVALUEDSP == null || model.getContactDetailsList!.sEGMENTVALUEDSP!.isEmpty)) { + model.getContactDetailsList!.sEGMENTVALUEDSP = model.eSERVICESVS!.first.vALUECOLUMNNAME; + model.getContactDetailsList!.vARCHAR2VALUE = model.eSERVICESVS!.first.iDCOLUMNNAME; + } + + String displayVal = model.getContactDetailsList!.sEGMENTVALUEDSP ?? ""; + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: PopupMenuButton( + child: DynamicTextFieldWidget( + label, + displayVal.isNotEmpty ? displayVal : LocaleKeys.select.tr(), + isEnable: false, + isPopup: true, + backgroundColor: Colors.white, + ), + itemBuilder: (_) => >[ + for (int i = 0; i < model.eSERVICESVS!.length; i++) + PopupMenuItem( + value: i, + child: (model.eSERVICESVS![i].vALUECOLUMNNAME ?? "").toText14(), + ), + ], + onSelected: (int selectedIdx) { + setState(() { + model.getContactDetailsList!.sEGMENTVALUEDSP = model.eSERVICESVS![selectedIdx].vALUECOLUMNNAME; + model.getContactDetailsList!.vARCHAR2VALUE = model.eSERVICESVS![selectedIdx].iDCOLUMNNAME; + }); + }, + ), + ); + } else { + var controller = _getControllerForField(colKey, model.getContactDetailsList!.sEGMENTVALUEDSP); + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: DynamicTextFieldWidget( + label, + LocaleKeys.enterFieldPlaceholder.tr(args: [label]), + controller: controller, + backgroundColor: Colors.white, + onChange: (String text) { + model.getContactDetailsList!.sEGMENTVALUEDSP = text; + model.getContactDetailsList!.vARCHAR2VALUE = text; + }, + ), + ); + } + } else if (model.fORMATTYPE == "N") { + var controller = _getControllerForField(colKey, model.getContactDetailsList!.sEGMENTVALUEDSP); + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: DynamicTextFieldWidget( + label, + LocaleKeys.enterFieldPlaceholder.tr(args: [label]), + controller: controller, + isInputTypeNum: true, + backgroundColor: Colors.white, + onChange: (String text) { + model.getContactDetailsList!.sEGMENTVALUEDSP = text; + model.getContactDetailsList!.vARCHAR2VALUE = text; + }, + ), + ); + } else if (model.fORMATTYPE == "X") { + String displayVal = model.getContactDetailsList!.sEGMENTVALUEDSP ?? ""; + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: DynamicTextFieldWidget( + label, + displayVal.isNotEmpty ? displayVal : LocaleKeys.dateFormatPlaceholder.tr(), + suffixIconData: Icons.calendar_today_outlined, + isReadOnly: true, + backgroundColor: Colors.white, + onTap: () async { + DateTime initial = DateTime.now(); + if (displayVal.isNotEmpty) { + try { + initial = DateFormat('yyyy-MM-dd').parse(displayVal); + } catch (_) { + try { + initial = DateFormat('dd/MM/yyyy').parse(displayVal); + } catch (_) {} + } + } + DateTime? picked = await _selectDate(context, initial); + if (picked != null) { + String formatted = DateFormat('yyyy-MM-dd', "en_US").format(picked); + setState(() { + model.getContactDetailsList!.sEGMENTVALUEDSP = formatted; + model.getContactDetailsList!.dATEVALUE = formatted; + model.getContactDetailsList!.vARCHAR2VALUE = formatted; + }); + } + }, + ), + ); + } + return const SizedBox.shrink(); + } + + // --- Build Dynamic Fields for Contact DFF --- + Widget _buildDynamicContactDffField(GetContactDffStructureList model, int index) { + if (model.dISPLAYFLAG == "N") { + return const SizedBox.shrink(); + } + + bool isRequired = model.rEQUIREDFLAG == "Y"; + String label = (model.sEGMENTPROMPT ?? "") + (isRequired ? "*" : ""); + model.getContactDetailsList ??= GetContactDetailsList(); + String colKey = model.aPPLICATIONCOLUMNNAME ?? "CONTACT_DFF_$index"; + + if (model.fORMATTYPE == "C") { + if (model.eSERVICESVS != null && model.eSERVICESVS!.isNotEmpty) { + String colName = (model.aPPLICATIONCOLUMNNAME ?? "").toUpperCase(); + String prompt = (model.sEGMENTPROMPT ?? "").toLowerCase(); + bool isSponsorRelated = colName.contains("SPONSOR") || prompt.contains("sponsor") || prompt.contains("كفيل"); + + bool valueInOptions = model.eSERVICESVS!.any((opt) => opt.vALUECOLUMNNAME == model.getContactDetailsList!.sEGMENTVALUEDSP); + if (isSponsorRelated && (!valueInOptions || model.getContactDetailsList!.sEGMENTVALUEDSP == null || model.getContactDetailsList!.sEGMENTVALUEDSP!.isEmpty)) { + model.getContactDetailsList!.sEGMENTVALUEDSP = model.eSERVICESVS!.first.vALUECOLUMNNAME; + model.getContactDetailsList!.vARCHAR2VALUE = model.eSERVICESVS!.first.iDCOLUMNNAME; + } + + String displayVal = model.getContactDetailsList!.sEGMENTVALUEDSP ?? ""; + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: PopupMenuButton( + child: DynamicTextFieldWidget( + label, + displayVal.isNotEmpty ? displayVal : LocaleKeys.select.tr(), + isEnable: false, + isPopup: true, + backgroundColor: Colors.white, + ), + itemBuilder: (_) => >[ + for (int i = 0; i < model.eSERVICESVS!.length; i++) + PopupMenuItem( + value: i, + child: (model.eSERVICESVS![i].vALUECOLUMNNAME ?? "").toText14(), + ), + ], + onSelected: (int selectedIdx) { + setState(() { + model.getContactDetailsList!.sEGMENTVALUEDSP = model.eSERVICESVS![selectedIdx].vALUECOLUMNNAME; + model.getContactDetailsList!.vARCHAR2VALUE = model.eSERVICESVS![selectedIdx].iDCOLUMNNAME; + }); + }, + ), + ); + } else { + var controller = _getControllerForField(colKey, model.getContactDetailsList!.sEGMENTVALUEDSP); + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: DynamicTextFieldWidget( + label, + LocaleKeys.enterFieldPlaceholder.tr(args: [label]), + controller: controller, + backgroundColor: Colors.white, + onChange: (String text) { + model.getContactDetailsList!.sEGMENTVALUEDSP = text; + model.getContactDetailsList!.vARCHAR2VALUE = text; + }, + ), + ); + } + } else if (model.fORMATTYPE == "N") { + var controller = _getControllerForField(colKey, model.getContactDetailsList!.sEGMENTVALUEDSP); + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: DynamicTextFieldWidget( + label, + LocaleKeys.enterFieldPlaceholder.tr(args: [label]), + controller: controller, + isInputTypeNum: true, + backgroundColor: Colors.white, + onChange: (String text) { + model.getContactDetailsList!.sEGMENTVALUEDSP = text; + model.getContactDetailsList!.vARCHAR2VALUE = text; + }, + ), + ); + } else if (model.fORMATTYPE == "X") { + String displayVal = model.getContactDetailsList?.dATEVALUE ?? model.getContactDetailsList?.sEGMENTVALUEDSP ?? ""; + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: DynamicTextFieldWidget( + label, + displayVal.isNotEmpty ? displayVal : LocaleKeys.dateFormatPlaceholder.tr(), + suffixIconData: Icons.calendar_today_outlined, + isReadOnly: true, + backgroundColor: Colors.white, + onTap: () async { + DateTime initial = DateTime.now(); + if (displayVal.isNotEmpty) { + try { + initial = DateFormat('yyyy-MM-dd').parse(displayVal); + } catch (_) { + try { + initial = DateFormat('dd/MM/yyyy').parse(displayVal); + } catch (_) {} + } + } + DateTime? picked = await _selectDate(context, initial); + if (picked != null) { + String formatted = DateFormat('yyyy-MM-dd', "en_US").format(picked); + setState(() { + model.getContactDetailsList!.sEGMENTVALUEDSP = formatted; + model.getContactDetailsList!.dATEVALUE = formatted; + model.getContactDetailsList!.vARCHAR2VALUE = formatted; + }); + } + }, + ), + ); + } + return const SizedBox.shrink(); + } + + // --- Step 2 Validation & Next --- + void _onNextFromBasicInfo() { + for (var col in _contactColsStructureList) { + if (col.dISPLAYFLAG != "N" && col.rEQUIREDFLAG == "Y") { + String? val = col.getContactDetailsList?.sEGMENTVALUEDSP; + String? varch = col.getContactDetailsList?.vARCHAR2VALUE; + if ((val == null || val.trim().isEmpty) && (varch == null || varch.trim().isEmpty)) { + String name = col.sEGMENTPROMPT ?? LocaleKeys.requiredField.tr(); + Utils.showToast(LocaleKeys.pleaseFillField.tr(args: [name])); + return; + } + } + } + for (var basic in _basicDetDffStructureList) { + if (basic.dISPLAYFLAG != "N" && basic.rEQUIREDFLAG == "Y") { + String? val = basic.getContactDetailsList?.sEGMENTVALUEDSP; + String? varch = basic.getContactDetailsList?.vARCHAR2VALUE; + if ((val == null || val.trim().isEmpty) && (varch == null || varch.trim().isEmpty)) { + String name = basic.sEGMENTPROMPT ?? LocaleKeys.requiredField.tr(); + Utils.showToast(LocaleKeys.pleaseFillField.tr(args: [name])); + return; + } + } + } + setState(() { + _currentStep = 3; // Move to Step 3: Additional Info + }); + } + + // --- Step 3 Validation & Next --- + void _onNextFromAdditionalInfo() { + for (var dff in _contactDffStructureList) { + if (dff.dISPLAYFLAG != "N" && dff.rEQUIREDFLAG == "Y") { + String? val = dff.getContactDetailsList?.sEGMENTVALUEDSP; + String? varch = dff.getContactDetailsList?.vARCHAR2VALUE; + if ((val == null || val.trim().isEmpty) && (varch == null || varch.trim().isEmpty)) { + String name = dff.sEGMENTPROMPT ?? LocaleKeys.requiredField.tr(); + Utils.showToast(LocaleKeys.pleaseFillField.tr(args: [name])); + return; + } + } + } + setState(() { + _currentStep = 4; // Move to Step 4: Submit / Review + }); + } + + // --- Step 4 Submit Transaction to EBS --- + void _onSubmitTransaction() async { + Utils.showLoading(context); + + try { + List> values1 = _basicDetDffStructureList.map((e) { + String? dateVal; + String? varChar; + int? numberVal; + + if (e.fORMATTYPE == 'N') { + dateVal = null; + varChar = null; + numberVal = int.tryParse(e.getContactDetailsList?.sEGMENTVALUEDSP ?? "0"); + } else if (e.fORMATTYPE == 'X') { + dateVal = e.getContactDetailsList?.sEGMENTVALUEDSP?.toString(); + varChar = null; + numberVal = null; + } else if (e.eSERVICESVS != null && e.eSERVICESVS!.isNotEmpty) { + dateVal = null; + varChar = e.getContactDetailsList?.vARCHAR2VALUE; + numberVal = null; + } else { + dateVal = null; + varChar = e.getContactDetailsList?.sEGMENTVALUEDSP; + numberVal = null; + } + return ValidateEitTransactionModel( + dATEVALUE: dateVal, + nAME: e.aPPLICATIONCOLUMNNAME, + nUMBERVALUE: numberVal, + tRANSACTIONNUMBER: 1, + vARCHAR2VALUE: varChar, + ).toJson(); + }).toList(); + + List> values2 = _contactDffStructureList.map((e) { + String? dateVal; + String? varChar; + int? numberVal; + + if (e.fORMATTYPE == 'N') { + dateVal = null; + varChar = null; + numberVal = int.tryParse(e.getContactDetailsList?.sEGMENTVALUEDSP ?? "0"); + } else if (e.fORMATTYPE == 'X') { + dateVal = e.getContactDetailsList?.dATEVALUE ?? e.getContactDetailsList?.sEGMENTVALUEDSP; + varChar = null; + numberVal = null; + } else if (e.eSERVICESVS != null && e.eSERVICESVS!.isNotEmpty) { + dateVal = null; + varChar = e.getContactDetailsList?.vARCHAR2VALUE; + numberVal = null; + } else { + dateVal = null; + numberVal = null; + varChar = e.getContactDetailsList?.sEGMENTVALUEDSP; + } + return ValidateEitTransactionModel( + dATEVALUE: dateVal, + nAME: e.aPPLICATIONCOLUMNNAME, + nUMBERVALUE: numberVal, + tRANSACTIONNUMBER: 1, + vARCHAR2VALUE: varChar, + ).toJson(); + }).toList(); + + List> values3 = _contactColsStructureList.map((e) { + String? dateVal; + String? varChar1; + int? numberVal; + + if (e.dATATYPE == 'VARCHAR2') { + dateVal = null; + numberVal = null; + if (e.objectValuesList != null && e.objectValuesList!.isNotEmpty) { + varChar1 = e.getContactDetailsList?.vARCHAR2VALUE ?? ""; + } else { + varChar1 = e.getContactDetailsList?.sEGMENTVALUEDSP ?? ""; + } + } else if (e.dATATYPE == 'DATE') { + dateVal = e.getContactDetailsList?.sEGMENTVALUEDSP?.toString(); + varChar1 = null; + numberVal = null; + } else if (e.dATATYPE == 'NUMBER') { + dateVal = null; + varChar1 = null; + numberVal = int.tryParse(e.getContactDetailsList?.sEGMENTVALUEDSP ?? "0"); + } + + // Format National ID for Oracle EBS (X-XXXX-XXXX-X) e.g. 2-5446-0227-5 + String colNameUpper = (e.aPPLICATIONCOLUMNNAME ?? '').toUpperCase().trim(); + if (colNameUpper.contains('NATIONAL_IDENTIFIER') || + colNameUpper.contains('CIVIL_ID') || + colNameUpper.contains('IQAMA')) { + if (varChar1 != null && varChar1.isNotEmpty) { + String clean = varChar1.replaceAll('-', '').trim(); + if (clean.length == 10) { + varChar1 = "${clean.substring(0, 1)}-${clean.substring(1, 5)}-${clean.substring(5, 9)}-${clean.substring(9, 10)}"; + } + } + } + + return ValidateEitTransactionModel( + dATEVALUE: dateVal, + nAME: e.aPPLICATIONCOLUMNNAME, + nUMBERVALUE: numberVal, + tRANSACTIONNUMBER: 1, + vARCHAR2VALUE: varChar1, + ).toJson(); + }).toList(); + + List> valuesFinal = [...values1, ...values2, ...values3]; + + SubmitContactTransactionList? submitResult = await ProfileApiClient().submitContactTransactionAddAndUpdate("ADD", null, valuesFinal); + + Utils.hideLoading(context); + + if (submitResult != null && submitResult.pTRANSACTIONID != null) { + await Navigator.pushNamed( + context, + AppRoutes.requestSubmitScreen, + arguments: RequestSubmitScreenParams( + LocaleKeys.profile_familyDetails.tr(), + submitResult.pTRANSACTIONID!, + submitResult.pITEMKEY ?? '', + 'family_member', + ), + ); + + if (!AppState().cancelRequestTrancsection) { + Navigator.pop(context); + return; + } + + Utils.showLoading(context); + await LeaveBalanceApiClient().cancelHrTransaction(submitResult.pTRANSACTIONID!); + Utils.hideLoading(context); + Navigator.pop(context); + } else { + Utils.showToast(LocaleKeys.dependentAddedSuccessfully.tr()); + Navigator.pop(context); + } + } catch (ex) { + Utils.hideLoading(context); + Utils.handleException(ex, context, null); + } + } + + @override + Widget build(BuildContext context) { + bool isAr = AppState().isArabic(context); + + return Scaffold( + backgroundColor: Colors.white, + appBar: AppBarWidget( + context, + title: LocaleKeys.addDependent.tr(), + showHomeButton: true, + onBackTapped: () { + if (_currentStep > 1) { + setState(() { + _currentStep--; + }); + } else { + Navigator.maybePop(context); + } + }, + onHomeTapped: () { + Navigator.pushNamedAndRemoveUntil( + context, + ApiConsts.ISDependetLogin ? AppRoutes.dashboardScreenForDependet : AppRoutes.dashboard, + (Route route) => false, + ); + }, + ), + body: SafeArea( + child: Column( + children: [ + // Stepper: 1 Verify -> 2 Basic -> 3 Additional -> 4 Submit + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), + child: _buildStepper(isAr), + ), + + // Step Content + Expanded( + child: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8), + child: _buildCurrentStepContent(isAr), + ), + ), + + // Pinned Bottom Button(s) - Reusing DefaultButton with gradient + _buildBottomButtons(isAr), + ], + ), + ), + ); + } + + // --- Stepper Widget --- + Widget _buildStepper(bool isAr) { + List> steps = >[ + {'num': '1', 'title': LocaleKeys.stepVerify.tr()}, + {'num': '2', 'title': LocaleKeys.stepBasic.tr()}, + {'num': '3', 'title': LocaleKeys.stepAdditional.tr()}, + {'num': '4', 'title': LocaleKeys.submit.tr()}, + ]; + + Color completedColor = const Color(0xFF199F6A); // completed colors as per figma + Color activeColor = const Color(0xFFFF8A00); + Color inactiveBorderColor = const Color(0xFFE2E8F0); + Color inactiveTextColor = const Color(0xFF64748B); + + const double circleSize = 32.0; + + return LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + double availableWidth = constraints.maxWidth; + // The stepper is left-aligned and spans ~75-80% of width on mobile + double stepSpacing = ((availableWidth * 0.78 - circleSize) / (steps.length - 1)).clamp(68.0, 84.0); + + return Align( + alignment: AlignmentDirectional.centerStart, + child: Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + for (int i = 0; i < steps.length; i++) ...[ + if (i < steps.length - 1) + SizedBox( + width: stepSpacing, + child: Stack( + clipBehavior: Clip.none, + children: [ + // Connecting line from right edge of circle to next circle + Positioned( + top: (circleSize - 2) / 2, + left: isAr ? null : circleSize, + right: isAr ? circleSize : null, + width: stepSpacing - circleSize, + child: Container( + height: 2, + color: (i + 1) < _currentStep ? completedColor : inactiveBorderColor, + ), + ), + // Circle + Title (left-aligned with circle) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + _buildStepCircle( + i: i, + steps: steps, + completedColor: completedColor, + activeColor: activeColor, + inactiveBorderColor: inactiveBorderColor, + inactiveTextColor: inactiveTextColor, + circleSize: circleSize, + ), + const SizedBox(height: 6), + steps[i]['title']!.toText12( + isBold: (i + 1) == _currentStep, + color: (i + 1) < _currentStep + ? completedColor + : ((i + 1) == _currentStep ? activeColor : inactiveTextColor), + ), + ], + ), + ], + ), + ) + else + // Last step (Submit) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + _buildStepCircle( + i: i, + steps: steps, + completedColor: completedColor, + activeColor: activeColor, + inactiveBorderColor: inactiveBorderColor, + inactiveTextColor: inactiveTextColor, + circleSize: circleSize, + ), + const SizedBox(height: 6), + steps[i]['title']!.toText12( + isBold: (i + 1) == _currentStep, + color: (i + 1) < _currentStep + ? completedColor + : ((i + 1) == _currentStep ? activeColor : inactiveTextColor), + ), + ], + ), + ], + ], + ), + ); + }, + ); + } + + Widget _buildStepCircle({ + required int i, + required List> steps, + required Color completedColor, + required Color activeColor, + required Color inactiveBorderColor, + required Color inactiveTextColor, + required double circleSize, + }) { + bool isCompleted = (i + 1) < _currentStep; + bool isActive = (i + 1) == _currentStep; + + return Container( + width: circleSize, + height: circleSize, + decoration: BoxDecoration( + color: isCompleted + ? completedColor + : (isActive ? activeColor : Colors.white), + shape: BoxShape.circle, + border: Border.all( + color: isCompleted + ? completedColor + : (isActive ? activeColor : inactiveBorderColor), + width: 1.5, + ), + ), + alignment: Alignment.center, + child: steps[i]['num']!.toText13( + isBold: true, + color: (isCompleted || isActive) ? Colors.white : inactiveTextColor, + ), + ); + } + + // --- Step Content Switcher --- + Widget _buildCurrentStepContent(bool isAr) { + switch (_currentStep) { + case 1: + return _buildStep1Verify(isAr); + case 2: + return _buildStep2BasicInfo(isAr); + case 3: + return _buildStep3AdditionalInfo(isAr); + case 4: + return _buildStep4Submit(isAr); + default: + return _buildStep1Verify(isAr); + } + } + + // --- STEP 1: VERIFY --- + Widget _buildStep1Verify(bool isAr) { + String dobDisplay = _selectedDob != null + ? DateFormat('dd / MM / yyyy').format(_selectedDob!) + : LocaleKeys.dateFormatPlaceholder.tr(); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 12), + LocaleKeys.findYourDependent.tr().toText20( + isBold: true, + color: const Color(0xFF1E293B), + ), + const SizedBox(height: 8), + (_isLegislationSA + ? LocaleKeys.findYourDependentDesc.tr() + : LocaleKeys.enterDependentDetailsToContinue.tr()) + .toText14( + color: const Color(0xFF64748B), + ), + const SizedBox(height: 24), + + // National ID + DynamicTextFieldWidget( + "${LocaleKeys.nationalId.tr()}*", + _isLegislationSA + ? LocaleKeys.enter10DigitNationalId.tr() + : LocaleKeys.enterFieldPlaceholder.tr(args: [LocaleKeys.nationalId.tr()]), + controller: _nationalIdController, + suffixIconData: Icons.badge_outlined, + isInputTypeNum: _isLegislationSA, + maxLength: _isLegislationSA ? 10 : 25, + backgroundColor: Colors.white, + inputFormatters: _isLegislationSA + ? [ + FilteringTextInputFormatter.digitsOnly, + LengthLimitingTextInputFormatter(10), + ] + : [ + LengthLimitingTextInputFormatter(25), + ], + ), + const SizedBox(height: 16), + + // Date of Birth + DynamicTextFieldWidget( + "${LocaleKeys.dateOfBirth.tr()}*", + dobDisplay, + isReadOnly: true, + backgroundColor: Colors.white, + suffixIconData: Icons.calendar_today_outlined, + onTap: () => _pickDateOfBirth(context), + ), + const SizedBox(height: 24), + ], + ); + } + + // --- STEP 2: BASIC INFO (DYNAMICALLY GENERATED FROM CONTACT COLS & BASIC DET DFF) --- + Widget _buildStep2BasicInfo(bool isAr) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 12), + LocaleKeys.basicInfo.tr().toText20( + isBold: true, + color: const Color(0xFF1E293B), + ), + const SizedBox(height: 6), + (_isLegislationSA ? LocaleKeys.basicInfoDesc.tr() : LocaleKeys.enterBasicDetailsDesc.tr()).toText14( + color: const Color(0xFF64748B), + ), + const SizedBox(height: 20), + + // Dynamic fields from Contact Cols Structure + for (int i = 0; i < _contactColsStructureList.length; i++) + _buildDynamicColField(_contactColsStructureList[i], i), + + // Dynamic fields from Basic Det DFF Structure + for (int i = 0; i < _basicDetDffStructureList.length; i++) + _buildDynamicBasicDffField(_basicDetDffStructureList[i], i), + + const SizedBox(height: 12), + ], + ); + } + + // --- STEP 3: ADDITIONAL INFO (DYNAMICALLY GENERATED FROM CONTACT DFF) --- + Widget _buildStep3AdditionalInfo(bool isAr) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 12), + LocaleKeys.additionalInfo.tr().toText20( + isBold: true, + color: const Color(0xFF1E293B), + ), + const SizedBox(height: 6), + LocaleKeys.additionalInfoDesc.tr().toText14( + color: const Color(0xFF64748B), + ), + const SizedBox(height: 20), + + // Dynamic fields from Contact DFF Structure + for (int i = 0; i < _contactDffStructureList.length; i++) + _buildDynamicContactDffField(_contactDffStructureList[i], i), + + const SizedBox(height: 12), + ], + ); + } + + // Helper to extract field value dynamically for Review summary + String _findFieldValue(List keywords, [List? excludeKeywords]) { + for (var item in _contactColsStructureList) { + String colName = (item.aPPLICATIONCOLUMNNAME ?? '').toUpperCase(); + String prompt = (item.sEGMENTPROMPT ?? '').toUpperCase(); + if (excludeKeywords != null && excludeKeywords.any((ex) => colName.contains(ex.toUpperCase()) || prompt.contains(ex.toUpperCase()))) { + continue; + } + for (var k in keywords) { + String key = k.toUpperCase(); + if (colName.contains(key) || prompt.contains(key)) { + String? val = item.getContactDetailsList?.sEGMENTVALUEDSP; + if (val != null && val.trim().isNotEmpty) return val.trim(); + String? v2 = item.getContactDetailsList?.vARCHAR2VALUE; + if (v2 != null && v2.trim().isNotEmpty) return v2.trim(); + } + } + } + for (var item in _basicDetDffStructureList) { + String colName = (item.aPPLICATIONCOLUMNNAME ?? '').toUpperCase(); + String prompt = (item.sEGMENTPROMPT ?? '').toUpperCase(); + if (excludeKeywords != null && excludeKeywords.any((ex) => colName.contains(ex.toUpperCase()) || prompt.contains(ex.toUpperCase()))) { + continue; + } + for (var k in keywords) { + String key = k.toUpperCase(); + if (colName.contains(key) || prompt.contains(key)) { + String? val = item.getContactDetailsList?.sEGMENTVALUEDSP; + if (val != null && val.trim().isNotEmpty) return val.trim(); + String? v2 = item.getContactDetailsList?.vARCHAR2VALUE; + if (v2 != null && v2.trim().isNotEmpty) return v2.trim(); + } + } + } + for (var item in _contactDffStructureList) { + String colName = (item.aPPLICATIONCOLUMNNAME ?? '').toUpperCase(); + String prompt = (item.sEGMENTPROMPT ?? '').toUpperCase(); + if (excludeKeywords != null && excludeKeywords.any((ex) => colName.contains(ex.toUpperCase()) || prompt.contains(ex.toUpperCase()))) { + continue; + } + for (var k in keywords) { + String key = k.toUpperCase(); + if (colName.contains(key) || prompt.contains(key)) { + String? val = item.getContactDetailsList?.sEGMENTVALUEDSP; + if (val != null && val.trim().isNotEmpty) return val.trim(); + String? v2 = item.getContactDetailsList?.vARCHAR2VALUE; + if (v2 != null && v2.trim().isNotEmpty) return v2.trim(); + } + } + } + return ''; + } + + // --- STEP 4: SUBMIT / REVIEW --- + Widget _buildStep4Submit(bool isAr) { + String natId = _findFieldValue(['NATIONAL_IDENTIFIER', 'CIVIL_ID', 'IQAMA']); + if (natId.isEmpty) natId = _nationalIdController.text; + + String fn = _findFieldValue(['FIRST_NAME']); + String ln = _findFieldValue(['LAST_NAME', 'FAMILY_NAME']); + String fullName = "$fn $ln".trim(); + if (fullName.isEmpty) { + fullName = _findFieldValue(['CONTACT_NAME', 'FULL_NAME']); + } + if (fullName.isEmpty && _patientInfo != null) { + fullName = isAr ? _patientInfo!.fullNameAr : _patientInfo!.fullNameEn; + } + + String arFn = _findFieldValue(['ARABIC_FIRST', 'FIRST_NAME_AR']); + String arLn = _findFieldValue(['ARABIC_FAMILY', 'LAST_NAME_AR', 'ARABIC_LAST']); + String arFullName = "$arFn $arLn".trim(); + if (arFullName.isEmpty && _patientInfo != null) { + arFullName = _patientInfo!.fullNameAr; + } + + String dobDisplay = _findFieldValue(['DATE_OF_BIRTH', 'DOB']); + if (dobDisplay.isEmpty && _selectedDob != null) { + dobDisplay = DateFormat('dd / MM / yyyy').format(_selectedDob!); + } + + String rel = _findFieldValue(['CONTACT_TYPE', 'RELATIONSHIP']); + String sponsor = _findFieldValue(['SPONSOR'], ['DETAIL', 'تفاصيل']); + String sponsorDetail = _findFieldValue(['SPONSOR_DETAIL', 'DEP_SPONSOR', 'تفاصيل كفيل', 'DETAIL']); + String insurance = _findFieldValue(['INSURANCE', 'MED']); + String startDate = _findFieldValue(['START', 'DATE_START']); + if (startDate.isEmpty) startDate = dobDisplay; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 12), + LocaleKeys.reviewAndSubmit.tr().toText20( + isBold: true, + color: const Color(0xFF1E293B), + ), + const SizedBox(height: 6), + LocaleKeys.reviewAndSubmitDesc.tr().toText14( + color: const Color(0xFF64748B), + ), + const SizedBox(height: 20), + + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: const Color(0xFFE2E8F0)), + boxShadow: const [ + BoxShadow( + color: Color(0x08000000), + blurRadius: 8, + offset: Offset(0, 2), + ), + ], + ), + child: Column( + children: [ + _buildReviewRow(LocaleKeys.nationalId.tr(), natId), + const Divider(height: 20), + _buildReviewRow(LocaleKeys.fullName.tr(), fullName), + if (arFullName.isNotEmpty) ...[ + const Divider(height: 20), + _buildReviewRow(LocaleKeys.arabicName.tr(), arFullName), + ], + const Divider(height: 20), + _buildReviewRow(LocaleKeys.dateOfBirth.tr(), dobDisplay), + if (rel.isNotEmpty) ...[ + const Divider(height: 20), + _buildReviewRow(LocaleKeys.relationship.tr(), rel), + ], + if (sponsor.isNotEmpty) ...[ + const Divider(height: 20), + _buildReviewRow(LocaleKeys.sponsor.tr(), sponsor), + ], + if (sponsorDetail.isNotEmpty) ...[ + const Divider(height: 20), + _buildReviewRow(LocaleKeys.dependentSponsorDetail.tr(), sponsorDetail), + ], + if (insurance.isNotEmpty) ...[ + const Divider(height: 20), + _buildReviewRow(LocaleKeys.insuranceEligibility.tr(), insurance), + ], + if (startDate.isNotEmpty) ...[ + const Divider(height: 20), + _buildReviewRow(LocaleKeys.relationshipStartDate.tr(), startDate), + ], + ], + ), + ), + const SizedBox(height: 24), + ], + ); + } + + Widget _buildReviewRow(String title, String value) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + flex: 4, + child: title.toText13( + color: const Color(0xFF64748B), + ), + ), + Expanded( + flex: 6, + child: (value.isNotEmpty ? value : "-").toText14( + isBold: true, + textAlign: TextAlign.end, + color: const Color(0xFF1E293B), + ), + ), + ], + ); + } + + // --- Bottom Buttons (Always reusing DefaultButton with app gradient!) --- + Widget _buildBottomButtons(bool isAr) { + if (_currentStep == 1) { + return DefaultButton( + _isLegislationSA ? LocaleKeys.searchUser.tr() : LocaleKeys.next.tr(), + _isLoading ? null : _onSearchUser, + iconData: _isLegislationSA ? Icons.search : null, + isTextExpanded: false, + height: 48, + borderRadius: 12, + ).insideContainer; + } + + VoidCallback onBack = () { + setState(() { + _currentStep--; + }); + }; + + VoidCallback onNext; + String nextLabel; + + if (_currentStep == 2) { + onNext = _onNextFromBasicInfo; + nextLabel = LocaleKeys.next.tr(); + } else if (_currentStep == 3) { + onNext = _onNextFromAdditionalInfo; + nextLabel = LocaleKeys.next.tr(); + } else { + onNext = _onSubmitTransaction; + nextLabel = LocaleKeys.submitRequest.tr(); + } + + return Container( + color: Colors.white, + padding: const EdgeInsets.only(top: 16, bottom: 16, right: 20, left: 20), + child: Row( + children: [ + // Back Button: reuses DefaultButton with outline styling + Expanded( + child: DefaultButton( + LocaleKeys.back.tr(), + onBack, + colors: const [Colors.white, Colors.white], + textColor: MyColors.gradiantEndColor, + border: Border.all(color: MyColors.gradiantEndColor, width: 1.5), + height: 48, + borderRadius: 12, + ), + ), + const SizedBox(width: 16), + // Next / Submit Button: reuses DefaultButton with canonical gradient + Expanded( + child: DefaultButton( + nextLabel, + onNext, + height: 48, + borderRadius: 12, + ), + ), + ], + ), + ); + } +} diff --git a/lib/ui/profile/family_members.dart b/lib/ui/profile/family_members.dart index 7b33d47..eb9ddc0 100644 --- a/lib/ui/profile/family_members.dart +++ b/lib/ui/profile/family_members.dart @@ -1,19 +1,16 @@ import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:mohem_flutter_app/api/profile_api_client.dart'; -import 'package:mohem_flutter_app/classes/colors.dart'; +import 'package:mohem_flutter_app/app_state/app_state.dart'; import 'package:mohem_flutter_app/classes/utils.dart'; import 'package:mohem_flutter_app/config/routes.dart'; -import 'package:mohem_flutter_app/extensions/int_extensions.dart'; import 'package:mohem_flutter_app/extensions/string_extensions.dart'; -import 'package:mohem_flutter_app/extensions/widget_extensions.dart'; import 'package:mohem_flutter_app/generated/locale_keys.g.dart'; import 'package:mohem_flutter_app/models/dashboard/menu_entries.dart'; import 'package:mohem_flutter_app/models/get_employee_contacts.model.dart'; import 'package:mohem_flutter_app/provider/dashboard_provider_model.dart'; import 'package:mohem_flutter_app/ui/profile/add_update_family_member.dart'; -import 'package:mohem_flutter_app/widgets/app_bar_widget.dart'; +import 'package:mohem_flutter_app/ui/profile/mazaya_access_result_screen.dart'; import 'package:mohem_flutter_app/widgets/button/default_button.dart'; import 'package:provider/provider.dart'; @@ -25,9 +22,13 @@ class FamilyMembers extends StatefulWidget { } class _FamilyMembersState extends State { - List getEmployeeContactsList = []; + List getEmployeeContactsList = []; + bool fromMazaya = false; + bool _argsLoaded = false; + bool _isLoading = false; - int? relationId; + final TextEditingController _searchController = TextEditingController(); + String _searchQuery = ""; @override void initState() { @@ -35,18 +36,131 @@ class _FamilyMembersState extends State { getEmployeeContacts(); } - void getEmployeeContacts() async { + @override + void didChangeDependencies() { + super.didChangeDependencies(); + if (!_argsLoaded) { + Object? args = ModalRoute.of(context)?.settings.arguments; + if (args is Map && args['fromMazaya'] == true) { + fromMazaya = true; + } + _argsLoaded = true; + } + } + + @override + void dispose() { + _searchController.dispose(); + super.dispose(); + } + + bool _isAccessAllowed(GetEmployeeContactsList contact) { + String? flag = contact.mAZAYAACCESSFLG ?? contact.mazayaAccess?.toString(); + return flag?.trim().toUpperCase() == 'Y'; + } + + Future _toggleMazayaAccess(GetEmployeeContactsList contact) async { + bool current = _isAccessAllowed(contact); + String action = current ? "Revoke" : "Grant"; + try { Utils.showLoading(context); - getEmployeeContactsList = await ProfileApiClient().getEmployeeContacts(); + await ProfileApiClient().processContactMazayaAccess( + accessAction: action, + contactRelationshipId: contact.cONTACTRELATIONSHIPID, + contactPersonId: contact.cONTACTPERSONID, + personId: contact.pERSONID, + ); + + List rawList = await ProfileApiClient().getEmployeeContacts(); + getEmployeeContactsList = rawList; + Utils.hideLoading(context); - setState(() {}); + + if (mounted) { + setState(() {}); + if (action == "Grant") { + Navigator.push( + context, + MaterialPageRoute( + builder: (BuildContext cxt) => MazayaAccessResultScreen( + isGranted: true, + dependentName: contact.cONTACTNAME ?? "", + dependentInitials: _getInitials(contact.cONTACTNAME), + ), + ), + ); + } else { + Utils.showToast(LocaleKeys.revokeMazayaAccess.tr()); + } + } } catch (ex) { Utils.hideLoading(context); Utils.handleException(ex, context, null); } } + String _getInitials(String? name) { + if (name == null || name.trim().isEmpty) return "D"; + List parts = name.trim().split(RegExp(r'\s+')).where((String p) => p.isNotEmpty).toList(); + if (parts.isEmpty) return "D"; + if (parts.length == 1) { + return parts.first.length >= 2 ? parts.first.substring(0, 2).toUpperCase() : parts.first.toUpperCase(); + } + return "${parts.first[0]}${parts.last[0]}".toUpperCase(); + } + + String _getIdEnding(GetEmployeeContactsList contact) { + String raw = contact.pERSONID?.toString() ?? + contact.cONTACTPERSONID?.toString() ?? + contact.cONTACTRELATIONSHIPID?.toString() ?? + ""; + if (raw.length > 4) { + return raw.substring(raw.length - 4); + } + if (raw.isNotEmpty) { + return raw.padLeft(4, '0'); + } + return ""; + } + + List get _filteredList { + if (_searchQuery.trim().isEmpty) return getEmployeeContactsList; + String q = _searchQuery.trim().toLowerCase(); + return getEmployeeContactsList.where((GetEmployeeContactsList item) { + String name = (item.cONTACTNAME ?? "").toLowerCase(); + String rel = (item.rELATIONSHIP ?? "").toLowerCase(); + String id = _getIdEnding(item); + String personId = (item.pERSONID?.toString() ?? "").toLowerCase(); + String contactPersonId = (item.cONTACTPERSONID?.toString() ?? "").toLowerCase(); + String contactType = (item.cONTACTTYPE ?? "").toLowerCase(); + return name.contains(q) || + rel.contains(q) || + id.contains(q) || + personId.contains(q) || + contactPersonId.contains(q) || + contactType.contains(q); + }).toList(); + } + + void getEmployeeContacts() async { + setState(() { + _isLoading = true; + }); + try { + List rawList = await ProfileApiClient().getEmployeeContacts(); + getEmployeeContactsList = rawList; + } catch (ex) { + Utils.handleException(ex, context, null); + } finally { + if (mounted) { + setState(() { + _isLoading = false; + }); + } + } + } + GetMenuEntriesList _getContactMenuEntries(BuildContext context) { List menuData = context.watch().getMenuEntriesList ?? []; List arr = menuData.where((GetMenuEntriesList e) => e.requestType == 'CONTACT').toList(); @@ -55,181 +169,424 @@ class _FamilyMembersState extends State { @override Widget build(BuildContext context) { + bool isAr = AppState().isArabic(context); GetMenuEntriesList menuEntries = _getContactMenuEntries(context); - bool canAddFamilyMember = menuEntries.addButton == 'Y'; + bool canAddFamilyMember = menuEntries.addButton == 'Y' || menuEntries.updateButton == 'Y'; bool canUpdateFamilyMember = menuEntries.updateButton == 'Y'; bool canDeleteFamilyMember = menuEntries.deleteButton == 'Y'; + List displayList = _filteredList; + return Scaffold( - appBar: AppBarWidget( - context, - title: LocaleKeys.profile_familyDetails.tr(), + backgroundColor: Colors.white, + appBar: PreferredSize( + preferredSize: const Size.fromHeight(56), + child: SafeArea( + child: Container( + height: 56, + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + children: [ + InkWell( + borderRadius: BorderRadius.circular(20), + onTap: () => Navigator.pop(context), + child: const Padding( + padding: EdgeInsets.all(8.0), + child: Icon( + Icons.arrow_back_ios_new_rounded, + size: 20, + color: Color(0xFF1E293B), + ), + ), + ), + const SizedBox(width: 8), + (fromMazaya + ? LocaleKeys.myDependents.tr() + : LocaleKeys.profile_familyDetails.tr()) + .toText20( + isBold: true, + color: const Color(0xFF1E293B), + ), + const Spacer(), + InkWell( + borderRadius: BorderRadius.circular(20), + onTap: () => Navigator.popUntil(context, ModalRoute.withName(AppRoutes.dashboard)), + child: const Padding( + padding: EdgeInsets.all(8.0), + child: Icon( + Icons.home_outlined, + size: 24, + color: Color(0xFF1E293B), + ), + ), + ), + ], + ), + ), ), - backgroundColor: MyColors.backgroundColor, - body: Column( - children: [ + ), + body: SafeArea( + child: Column( + children: [ + // Search Bar + Padding( + padding: const EdgeInsets.fromLTRB(20, 8, 20, 12), + child: Container( + height: 48, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: const Color(0xFFE2E8F0)), + ), + padding: const EdgeInsets.symmetric(horizontal: 14), + child: Row( + children: [ + const Icon(Icons.search, color: Color(0xFF64748B), size: 20), + const SizedBox(width: 10), + Expanded( + child: TextField( + controller: _searchController, + onChanged: (String val) { + setState(() { + _searchQuery = val; + }); + }, + decoration: InputDecoration( + border: InputBorder.none, + isDense: true, + contentPadding: EdgeInsets.zero, + hintText: fromMazaya + ? LocaleKeys.searchDependents.tr() + : LocaleKeys.searchFamilyMembers.tr(), + hintStyle: const TextStyle(color: Color(0xFF94A3B8), fontSize: 14), + ), + ), + ), + if (_searchQuery.isNotEmpty) + InkWell( + onTap: () { + _searchController.clear(); + setState(() { + _searchQuery = ""; + }); + }, + child: const Padding( + padding: EdgeInsets.symmetric(horizontal: 4), + child: Icon(Icons.close, color: Color(0xFF94A3B8), size: 18), + ), + ), + ], + ), + ), + ), + + // Dependents list Expanded( - child: getEmployeeContactsList.isEmpty - ? Utils.getNoDataWidget(context) - : ListView.separated( - padding: const EdgeInsets.all(21), - separatorBuilder: (cxt, index) => 12.height, - itemCount: getEmployeeContactsList.length, - itemBuilder: (context, index) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - "${getEmployeeContactsList[index].cONTACTNAME}".toText16(color: MyColors.grey3AColor), - "${getEmployeeContactsList[index].rELATIONSHIP}".toText11(color: MyColors.textMixColor), - ]).paddingOnly(top: 15, left: 13, right: 13, bottom: 12), - 1.divider, - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - // todo @faitma clean below logic and write clean code. - RichText( - text: TextSpan( - children: [ - WidgetSpan( - child: Icon( - Icons.edit, - size: 14, - color: canUpdateFamilyMember ? MyColors.grey67Color : MyColors.lightGreyColor, - )), - TextSpan( - text: LocaleKeys.update.tr(), - style: TextStyle( - color: canUpdateFamilyMember ? MyColors.grey67Color : MyColors.lightGreyColor, fontSize: 12, letterSpacing: -0.36, fontWeight: FontWeight.w600), - recognizer: TapGestureRecognizer() - ..onTap = () async { - relationId = getEmployeeContactsList[index].cONTACTRELATIONSHIPID; - if (canUpdateFamilyMember && relationId != null) { - showUpdateAlertDialog(context, relationId, 2, "UPDATE"); - } - }) - ], - ), - ), - Container(height: 35, width: 1, color: const Color(0xffEFEFEF)), - RichText( - text: TextSpan( - children: [ - WidgetSpan( - child: Icon( - Icons.delete, - size: 15, - color: canDeleteFamilyMember ? MyColors.redColor : MyColors.lightGreyColor, - ), - ), - TextSpan( - text: LocaleKeys.remove.tr(), - style: TextStyle( - color: canDeleteFamilyMember ? MyColors.redColor : MyColors.lightGreyColor, fontSize: 12, letterSpacing: -0.36, fontWeight: FontWeight.w600), - ), - ], - ), - ).onPress(() { - relationId = getEmployeeContactsList[index].cONTACTRELATIONSHIPID; - if (canDeleteFamilyMember && relationId != null) { - showRemoveAlertDialog(context, relationId); - } - }), - ], - ), - ], - ).objectContainerView(disablePadding: true); - }), + child: _isLoading + ? Center( + child: Image.asset( + "assets/images/logos/loading_mohemm_logo.gif", + height: 96.0, + width: 96.0, + ), + ) + : displayList.isEmpty + ? Utils.getNoDataWidget(context) + : ListView.separated( + padding: const EdgeInsets.fromLTRB(20, 4, 20, 20), + separatorBuilder: (BuildContext cxt, int index) => const SizedBox(height: 12), + itemCount: displayList.length, + itemBuilder: (BuildContext context, int index) { + return _buildDependentCard( + displayList[index], + isAr, + canUpdateFamilyMember, + canDeleteFamilyMember, + ); + }, + ), ), + + // Bottom action button: + Add another user DefaultButton( - LocaleKeys.addNewFamilyMember.tr(), - canAddFamilyMember - ? () async { - Navigator.pushNamed( - context, - AppRoutes.addUpdateFamilyMember, - arguments: FamilyMemberScreenParams(null, flag: 1, actionType: "ADD"), - ); + fromMazaya + ? LocaleKeys.addAnotherUser.tr() + : LocaleKeys.addNewFamilyMember.tr(), + (canAddFamilyMember || fromMazaya) + ? () async { + await Navigator.pushNamed( + context, + AppRoutes.addDependentVerify, + arguments: {'fromMazaya': fromMazaya}, + ); + getEmployeeContacts(); + } + : null, + iconData: Icons.add, + isTextExpanded: false, + height: 48, + borderRadius: 12, + ).insideContainer, + ], + ), + ), + ); + } + + Widget _buildDependentCard( + GetEmployeeContactsList item, + bool isAr, + bool canUpdate, + bool canDelete, + ) { + bool isAllowed = _isAccessAllowed(item); + + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: const Color(0xFFF1F5F9)), + boxShadow: const [ + BoxShadow( + color: Color(0x0A000000), + blurRadius: 8, + offset: Offset(0, 3), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Top row: Avatar, Info, and Status Badge + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // Initials Circle + Container( + width: 44, + height: 44, + decoration: const BoxDecoration( + color: Color(0xFFE6F7F2), + shape: BoxShape.circle, + ), + child: Center( + child: _getInitials(item.cONTACTNAME).toText16( + isBold: true, + color: const Color(0xFF168A89), + ), + ), + ), + const SizedBox(width: 12), + + // Name & Details + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + (item.cONTACTNAME ?? "").toText16( + isBold: true, + maxlines: 1, + color: const Color(0xFF1E293B), + ), + const SizedBox(height: 3), + Row( + children: [ + Flexible( + child: (item.rELATIONSHIP ?? "").toText12( + maxLine: 1, + color: const Color(0xFF168A89), + ), + ), + + ], + ), + ], + ), + ), + + // Top right pill (shown in Mazaya flow) + if (fromMazaya) ...[ + const SizedBox(width: 8), + Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + decoration: BoxDecoration( + color: isAllowed ? const Color(0xFFE8F8F0) : const Color(0xFFFEECEB), + borderRadius: BorderRadius.circular(20), + ), + child: (isAllowed ? LocaleKeys.active.tr() : LocaleKeys.noAccess.tr()).toText11( + color: isAllowed ? const Color(0xFF2DBD72) : const Color(0xFFED1C24), + ), + ), + ], + ], + ), + + const SizedBox(height: 14), + + // Bottom Action Row + if (fromMazaya) ...[ + // Mazaya Access Buttons: Revoke or Allow + InkWell( + borderRadius: BorderRadius.circular(10), + onTap: () => _toggleMazayaAccess(item), + child: Container( + width: double.infinity, + height: 40, + decoration: BoxDecoration( + color: isAllowed ? const Color(0xFFFFF1F0) : const Color(0xFFE6F7F2), + borderRadius: BorderRadius.circular(10), + border: Border.all( + color: isAllowed ? const Color(0xFFFFD9D7) : const Color(0xFFCEEFE7), + width: 0.8, + ), + ), + child: Center( + child: (isAllowed + ? LocaleKeys.revokeMazayaAccess.tr() + : LocaleKeys.allowMazayaAccess.tr()) + .toText14( + isBold: true, + isCenter: true, + color: isAllowed ? const Color(0xFFED1C24) : const Color(0xFF168A89), + ), + ), + ), + ), + ] else ...[ + // Profile Flow: Update and Remove action buttons + Row( + children: [ + Expanded( + child: InkWell( + borderRadius: BorderRadius.circular(10), + onTap: canUpdate + ? () { + int? relationId = item.cONTACTRELATIONSHIPID; + if (relationId != null) { + showUpdateAlertDialog(context, relationId, 2, "UPDATE"); + } + } + : null, + child: Container( + height: 38, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(10), + border: Border.all( + color: canUpdate ? const Color(0xFFCBD5E1) : const Color(0xFFE2E8F0), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.edit_outlined, + size: 15, + color: canUpdate ? const Color(0xFF475569) : const Color(0xFF94A3B8), + ), + const SizedBox(width: 6), + LocaleKeys.update.tr().toText12( + color: canUpdate ? const Color(0xFF475569) : const Color(0xFF94A3B8), + ), + ], + ), + ), + ), + ), + const SizedBox(width: 10), + Expanded( + child: InkWell( + borderRadius: BorderRadius.circular(10), + onTap: canDelete + ? () { + int? relationId = item.cONTACTRELATIONSHIPID; + if (relationId != null) { + showRemoveAlertDialog(context, relationId); + } } - : null) - .insideContainer, + : null, + child: Container( + height: 38, + decoration: BoxDecoration( + color: canDelete ? const Color(0xFFFFF5F5) : Colors.white, + borderRadius: BorderRadius.circular(10), + border: Border.all( + color: canDelete ? const Color(0xFFFFCDD2) : const Color(0xFFE2E8F0), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.delete_outline_rounded, + size: 15, + color: canDelete ? const Color(0xFFED1C24) : const Color(0xFF94A3B8), + ), + const SizedBox(width: 6), + LocaleKeys.remove.tr().toText12( + color: canDelete ? const Color(0xFFED1C24) : const Color(0xFF94A3B8), + ), + ], + ), + ), + ), + ), + ], + ), ], - )); + ], + ), + ); } - void showUpdateAlertDialog(BuildContext context, relationId, int flag, String actionType) { + void showUpdateAlertDialog(BuildContext context, int relationId, int flag, String actionType) { Widget cancelButton = TextButton( - child: Text( - LocaleKeys.cancel.tr(), - ), - onPressed: () { - Navigator.pop(context); - }, + child: LocaleKeys.cancel.tr().toText14(), + onPressed: () => Navigator.pop(context), ); Widget continueButton = TextButton( - child: Text( - LocaleKeys.ok.tr(), - ), + child: LocaleKeys.ok.tr().toText14(), onPressed: () { Navigator.pop(context); Navigator.pushNamed( context, AppRoutes.addUpdateFamilyMember, - arguments:FamilyMemberScreenParams(relationId, flag: 2, actionType: "UPDATE"), - ); + arguments: FamilyMemberScreenParams(relationId, flag: 2, actionType: "UPDATE"), + ).then((_) => getEmployeeContacts()); }, ); AlertDialog alert = AlertDialog( - title: Text( - LocaleKeys.confirm.tr(), - ), - content: Text(LocaleKeys.updateMember.tr()), - actions: [ - cancelButton, - continueButton, - ], + title: LocaleKeys.confirm.tr().toText16(isBold: true), + content: LocaleKeys.updateMember.tr().toText14(), + actions: [cancelButton, continueButton], ); showDialog( context: context, - builder: (BuildContext context) { - return alert; - }, + builder: (BuildContext context) => alert, ); } - void showRemoveAlertDialog(BuildContext context, relationId) { + void showRemoveAlertDialog(BuildContext context, int relationId) { Widget cancelButton = TextButton( - child: Text( - LocaleKeys.cancel.tr(), - ), - onPressed: () { - Navigator.pop(context); - }, + child: LocaleKeys.cancel.tr().toText14(), + onPressed: () => Navigator.pop(context), ); Widget continueButton = TextButton( - child: Text( - LocaleKeys.ok.tr(), - ), + child: LocaleKeys.ok.tr().toText14(), onPressed: () { - Navigator.pushNamed(context, AppRoutes.deleteFamilyMember, arguments: relationId); - // continueDynamicForms(); + Navigator.pop(context); + Navigator.pushNamed(context, AppRoutes.deleteFamilyMember, arguments: relationId).then((_) => getEmployeeContacts()); }, ); AlertDialog alert = AlertDialog( - title: Text( - LocaleKeys.confirm.tr(), - ), - content: Text(LocaleKeys.removeThisMember.tr()), - actions: [ - cancelButton, - continueButton, - ], + title: LocaleKeys.confirm.tr().toText16(isBold: true), + content: LocaleKeys.removeThisMember.tr().toText14(), + actions: [cancelButton, continueButton], ); showDialog( context: context, - builder: (BuildContext context) { - return alert; - }, + builder: (BuildContext context) => alert, ); } } diff --git a/lib/ui/profile/mazaya_access_result_screen.dart b/lib/ui/profile/mazaya_access_result_screen.dart new file mode 100644 index 0000000..032d84e --- /dev/null +++ b/lib/ui/profile/mazaya_access_result_screen.dart @@ -0,0 +1,674 @@ +import 'dart:io'; + +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:mohem_flutter_app/app_state/app_state.dart'; +import 'package:mohem_flutter_app/classes/utils.dart'; +import 'package:mohem_flutter_app/extensions/string_extensions.dart'; +import 'package:mohem_flutter_app/generated/locale_keys.g.dart'; +import 'package:mohem_flutter_app/widgets/button/default_button.dart'; +import 'package:share_plus/share_plus.dart'; + +class MazayaAccessResultParams { + final bool isGranted; + final String dependentName; + final String? dependentInitials; + final String? downloadUrl; + + const MazayaAccessResultParams({ + required this.isGranted, + required this.dependentName, + this.dependentInitials, + this.downloadUrl, + }); +} + +class _ShieldPainter extends CustomPainter { + final Color color; + final bool isGranted; + + _ShieldPainter({required this.color, required this.isGranted}); + + @override + void paint(Canvas canvas, Size size) { + Paint paint = Paint() + ..color = color + ..style = PaintingStyle.stroke + ..strokeWidth = 2.4 + ..strokeCap = StrokeCap.round + ..strokeJoin = StrokeJoin.round; + + double w = size.width; + double h = size.height; + + // Shield path + Path path = Path(); + path.moveTo(w * 0.5, h * 0.1); + path.cubicTo(w * 0.72, h * 0.1, w * 0.88, h * 0.18, w * 0.88, h * 0.38); + path.cubicTo(w * 0.88, h * 0.65, w * 0.68, h * 0.84, w * 0.5, h * 0.92); + path.cubicTo(w * 0.32, h * 0.84, w * 0.12, h * 0.65, w * 0.12, h * 0.38); + path.cubicTo(w * 0.12, h * 0.18, w * 0.28, h * 0.1, w * 0.5, h * 0.1); + path.close(); + + canvas.drawPath(path, paint); + + if (isGranted) { + // Checkmark path + Path checkPath = Path(); + checkPath.moveTo(w * 0.34, h * 0.49); + checkPath.lineTo(w * 0.46, h * 0.61); + checkPath.lineTo(w * 0.66, h * 0.39); + canvas.drawPath(checkPath, paint); + } else { + // Cross path + canvas.drawLine( + Offset(w * 0.38, h * 0.38), + Offset(w * 0.62, h * 0.62), + paint, + ); + canvas.drawLine( + Offset(w * 0.62, h * 0.38), + Offset(w * 0.38, h * 0.62), + paint, + ); + } + } + + @override + bool shouldRepaint(covariant _ShieldPainter oldDelegate) => + color != oldDelegate.color || isGranted != oldDelegate.isGranted; +} + +class MazayaAccessResultScreen extends StatelessWidget { + final bool isGranted; + final String dependentName; + final String? dependentInitials; + final String? downloadUrl; + + const MazayaAccessResultScreen({ + Key? key, + this.isGranted = true, + this.dependentName = "", + this.dependentInitials, + this.downloadUrl, + }) : super(key: key); + + static String getDownloadLink() { + int channel = AppState().postParamsObject?.channel ?? 31; + bool isIos = Platform.isIOS; + if (channel == 33) { + return isIos + ? "https://apps.apple.com/sa/app/al-habib-mohem/id1480157255" + : "https://play.google.com/store/apps/details?id=com.cloudsolutions.alhabibmohemm&hl=en"; + } else { + return isIos + ? "https://apps.apple.com/sa/app/mohemm/id1468856602" + : "https://play.google.com/store/apps/details?id=hmg.cloudSolutions.mohem&hl=en"; + } + } + + String _getInitials(String? name) { + if (name == null || name.trim().isEmpty) return "D"; + List parts = name.trim().split(RegExp(r'\s+')).where((String p) => p.isNotEmpty).toList(); + if (parts.isEmpty) return "D"; + if (parts.length == 1) { + return parts.first.length >= 2 ? parts.first.substring(0, 2).toUpperCase() : parts.first.toUpperCase(); + } + return "${parts.first[0]}${parts.last[0]}".toUpperCase(); + } + + @override + Widget build(BuildContext context) { + bool isAr = AppState().isArabic(context); + String fontFamily = isAr ? 'Cairo' : 'Poppins'; + + // Resolve arguments if passed via ModalRoute + Object? routeArgs = ModalRoute.of(context)?.settings.arguments; + bool effectiveGranted = isGranted; + String effectiveName = dependentName; + String? effectiveInitials = dependentInitials; + String? effectiveUrl = downloadUrl; + + if (routeArgs is MazayaAccessResultParams) { + effectiveGranted = routeArgs.isGranted; + effectiveName = routeArgs.dependentName; + effectiveInitials = routeArgs.dependentInitials; + effectiveUrl = routeArgs.downloadUrl; + } else if (routeArgs is Map) { + effectiveGranted = routeArgs['isGranted'] ?? isGranted; + effectiveName = routeArgs['dependentName'] ?? dependentName; + effectiveInitials = routeArgs['dependentInitials'] ?? dependentInitials; + effectiveUrl = routeArgs['downloadUrl'] ?? downloadUrl; + } + + String linkToShare = effectiveUrl ?? getDownloadLink(); + String initials = effectiveInitials ?? _getInitials(effectiveName); + + return Scaffold( + backgroundColor: Colors.white, + appBar: PreferredSize( + preferredSize: const Size.fromHeight(56), + child: SafeArea( + child: Container( + height: 56, + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + children: [ + InkWell( + borderRadius: BorderRadius.circular(20), + onTap: () => Navigator.pop(context), + child: const Padding( + padding: EdgeInsets.all(8.0), + child: Icon( + Icons.arrow_back_ios_new_rounded, + size: 20, + color: Color(0xFF1E293B), + ), + ), + ), + const SizedBox(width: 4), + LocaleKeys.mazayaTitle.tr().toText20( + isBold: true, + color: const Color(0xFF1E293B), + ), + const Spacer(), + InkWell( + borderRadius: BorderRadius.circular(20), + onTap: () => Navigator.pop(context), + child: const Padding( + padding: EdgeInsets.all(8.0), + child: Icon( + Icons.close_rounded, + size: 24, + color: Color(0xFF1E293B), + ), + ), + ), + ], + ), + ), + ), + ), + body: SafeArea( + child: Column( + children: [ + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.fromLTRB(20, 16, 20, 20), + child: effectiveGranted + ? _buildGrantedContent(context, isAr, fontFamily, effectiveName, initials, linkToShare) + : _buildRevokedContent(context, isAr, fontFamily, effectiveName, initials), + ), + ), + _buildBottomButtons(context, isAr, fontFamily, effectiveGranted, linkToShare), + ], + ), + ), + ); + } + + Widget _buildGrantedContent( + BuildContext context, + bool isAr, + String fontFamily, + String name, + String initials, + String linkToShare, + ) { + return Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox(height: 8), + + // Shield checkmark badge + Container( + width: 80, + height: 80, + decoration: const BoxDecoration( + color: Color(0xFFE6F7F2), + shape: BoxShape.circle, + ), + child: Center( + child: CustomPaint( + size: const Size(40, 40), + painter: _ShieldPainter( + color: const Color(0xFF168A89), + isGranted: true, + ), + ), + ), + ), + + const SizedBox(height: 18), + + // Title + LocaleKeys.mazayaAccessGranted.tr().toText22( + isBold: true, + isCentered: true, + color: const Color(0xFF1E293B), + ), + + const SizedBox(height: 8), + + // Description + LocaleKeys.mazayaAccessGrantedDesc.tr(args: [name]).toText14( + isCenter: true, + color: const Color(0xFF64748B), + ), + + const SizedBox(height: 22), + + // Access is now active banner + Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), + decoration: BoxDecoration( + color: const Color(0xFFE8F8F0), + borderRadius: BorderRadius.circular(14), + border: Border.all(color: const Color(0xFFCEEFE7), width: 1), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Padding( + padding: EdgeInsets.only(top: 2), + child: Icon( + Icons.check_circle_outline_rounded, + color: Color(0xFF168A89), + size: 20, + ), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LocaleKeys.accessIsNowActive.tr().toText14( + isBold: true, + color: const Color(0xFF168A89), + ), + const SizedBox(height: 3), + LocaleKeys.accessIsNowActiveDesc.tr(args: [name]).toText12( + color: const Color(0xFF64748B), + ), + ], + ), + ), + ], + ), + ), + + const SizedBox(height: 14), + + // Dependent Card + Container( + width: double.infinity, + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(14), + border: Border.all(color: const Color(0xFFF1F5F9)), + boxShadow: const [ + BoxShadow( + color: Color(0x08000000), + blurRadius: 10, + offset: Offset(0, 3), + ), + ], + ), + child: Row( + children: [ + Container( + width: 42, + height: 42, + decoration: const BoxDecoration( + color: Color(0xFFE6F7F2), + shape: BoxShape.circle, + ), + child: Center( + child: initials.toText14( + isBold: true, + color: const Color(0xFF168A89), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + name.toText14( + isBold: true, + maxlines: 1, + color: const Color(0xFF1E293B), + ), + const SizedBox(height: 2), + LocaleKeys.dependentAccessActive.tr().toText12( + isBold: true, + color: const Color(0xFF168A89), + ), + ], + ), + ), + const Icon( + Icons.lock_open_rounded, + color: Color(0xFF168A89), + size: 20, + ), + ], + ), + ), + + const SizedBox(height: 14), + + // Shareable App-Download Link Card + Container( + width: double.infinity, + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(14), + border: Border.all(color: const Color(0xFFE2E8F0)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: const Color(0xFFE8F8F0), + borderRadius: BorderRadius.circular(10), + ), + child: const Center( + child: Icon( + Icons.smartphone_rounded, + color: Color(0xFF168A89), + size: 20, + ), + ), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LocaleKeys.shareableAppDownloadLink.tr().toText14( + isBold: true, + color: const Color(0xFF1E293B), + ), + const SizedBox(height: 2), + LocaleKeys.shareableAppDownloadLinkDesc.tr(args: [name]).toText12( + color: const Color(0xFF64748B), + ), + ], + ), + ), + ], + ), + const SizedBox(height: 12), + Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 9), + decoration: BoxDecoration( + color: const Color(0xFFF8FAFC), + borderRadius: BorderRadius.circular(10), + border: Border.all(color: const Color(0xFFE2E8F0)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LocaleKeys.downloadLink.tr().toText11( + isBold: true, + color: const Color(0xFF168A89), + ), + const SizedBox(height: 3), + linkToShare.toText12( + maxLine: 1, + color: const Color(0xFF334155), + ), + ], + ), + ), + ], + ), + ), + ], + ); + } + + Widget _buildRevokedContent( + BuildContext context, + bool isAr, + String fontFamily, + String name, + String initials, + ) { + return Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox(height: 8), + + // Shield bad badge + Container( + width: 80, + height: 80, + decoration: const BoxDecoration( + color: Color(0xFFFEECEB), + shape: BoxShape.circle, + ), + child: Center( + child: CustomPaint( + size: const Size(40, 40), + painter: _ShieldPainter( + color: const Color(0xFFED1C24), + isGranted: false, + ), + ), + ), + ), + + const SizedBox(height: 18), + + // Title + LocaleKeys.mazayaAccessRevoked.tr().toText22( + isBold: true, + isCentered: true, + color: const Color(0xFF1E293B), + ), + + const SizedBox(height: 8), + + // Description + LocaleKeys.mazayaAccessRevokedDesc.tr().toText14( + isCenter: true, + color: const Color(0xFF64748B), + ), + + const SizedBox(height: 22), + + // Contact your sponsor banner + Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), + decoration: BoxDecoration( + color: const Color(0xFFFFF5F5), + borderRadius: BorderRadius.circular(14), + border: Border.all(color: const Color(0xFFFFCDD2), width: 1), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Padding( + padding: EdgeInsets.only(top: 2), + child: Icon( + Icons.phone_outlined, + color: Color(0xFFED1C24), + size: 20, + ), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LocaleKeys.contactYourSponsor.tr().toText14( + isBold: true, + color: const Color(0xFFED1C24), + ), + const SizedBox(height: 3), + LocaleKeys.contactYourSponsorDesc.tr().toText12( + color: const Color(0xFF64748B), + ), + ], + ), + ), + ], + ), + ), + + const SizedBox(height: 14), + + // Dependent Card + Container( + width: double.infinity, + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(14), + border: Border.all(color: const Color(0xFFF1F5F9)), + boxShadow: const [ + BoxShadow( + color: Color(0x08000000), + blurRadius: 10, + offset: Offset(0, 3), + ), + ], + ), + child: Row( + children: [ + Container( + width: 42, + height: 42, + decoration: const BoxDecoration( + color: Color(0xFFFFF1F0), + shape: BoxShape.circle, + ), + child: Center( + child: initials.toText14( + isBold: true, + color: const Color(0xFFED1C24), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + name.toText14( + isBold: true, + maxlines: 1, + color: const Color(0xFF1E293B), + ), + const SizedBox(height: 2), + LocaleKeys.dependentAccessBlocked.tr().toText12( + isBold: true, + color: const Color(0xFFED1C24), + ), + ], + ), + ), + const Icon( + Icons.lock_outline_rounded, + color: Color(0xFFED1C24), + size: 20, + ), + ], + ), + ), + ], + ); + } + + Widget _buildBottomButtons( + BuildContext context, + bool isAr, + String fontFamily, + bool granted, + String linkToShare, + ) { + if (granted) { + return Container( + color: Colors.white, + padding: const EdgeInsets.fromLTRB(20, 10, 20, 20), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + DefaultButton( + LocaleKeys.shareDownloadLink.tr(), + () async { + try { + await SharePlus.instance.share(ShareParams(text: linkToShare)); + } catch (_) {} + }, + trailingIconData: isAr ? Icons.arrow_back_rounded : Icons.arrow_forward_rounded, + fontSize: 14, + isTextExpanded: false, + height: 48, + borderRadius: 12, + ), + const SizedBox(height: 10), + DefaultButton( + LocaleKeys.copyLink.tr(), + () { + Clipboard.setData(ClipboardData(text: linkToShare)); + Utils.showToast(LocaleKeys.linkCopied.tr()); + }, + colors: const [Colors.white, Colors.white], + border: Border.all(color: const Color(0xFFCBD5E1)), + textColor: const Color(0xFF168A89), + fontSize: 14, + isTextExpanded: false, + height: 48, + borderRadius: 12, + ), + const SizedBox(height: 10), + DefaultButton( + LocaleKeys.done.tr(), + () => Navigator.pop(context), + colors: const [Colors.white, Colors.white], + border: Border.all(color: const Color(0xFFCBD5E1)), + textColor: const Color(0xFF334155), + fontSize: 14, + isTextExpanded: false, + height: 48, + borderRadius: 12, + ), + ], + ), + ); + } else { + return Container( + color: Colors.white, + padding: const EdgeInsets.fromLTRB(20, 10, 20, 20), + child: DefaultButton( + LocaleKeys.returnToSignIn.tr(), + () => Navigator.pop(context), + colors: const [Colors.white, Colors.white], + border: Border.all(color: const Color(0xFFCEEFE7), width: 1.2), + textColor: const Color(0xFF168A89), + iconData: isAr ? Icons.arrow_forward_rounded : Icons.arrow_back_rounded, + fontSize: 14, + isTextExpanded: false, + height: 48, + borderRadius: 12, + ), + ); + } + } +} diff --git a/lib/widgets/app_in_app_webview_screen.dart b/lib/widgets/app_in_app_webview_screen.dart new file mode 100644 index 0000000..f653d79 --- /dev/null +++ b/lib/widgets/app_in_app_webview_screen.dart @@ -0,0 +1,153 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_inappwebview/flutter_inappwebview.dart'; +import 'package:mohem_flutter_app/classes/consts.dart'; +import 'package:mohem_flutter_app/config/routes.dart'; +import 'package:mohem_flutter_app/generated/locale_keys.g.dart'; +import 'package:mohem_flutter_app/widgets/app_bar_widget.dart'; +import 'package:permission_handler/permission_handler.dart'; + +class AppInAppWebViewScreen extends StatefulWidget { + final String url; + final String? title; + + const AppInAppWebViewScreen({ + Key? key, + required this.url, + this.title, + }) : super(key: key); + + @override + State createState() => _AppInAppWebViewScreenState(); +} + +class _AppInAppWebViewScreenState extends State { + InAppWebViewController? _webViewController; + double _progress = 0; + String _title = ""; + + @override + void initState() { + super.initState(); + _title = widget.title ?? ""; + _requestCameraPermission(); + } + + Future _requestCameraPermission() async { + try { + PermissionStatus cameraStatus = await Permission.camera.status; + if (!cameraStatus.isGranted) { + await Permission.camera.request(); + } + } catch (e) { + debugPrint("Error requesting camera permission: $e"); + } + } + + void _onHomeTapped() { + if (ApiConsts.isDependetLogin) { + Navigator.popUntil(context, ModalRoute.withName(AppRoutes.dashboardScreenForDependet)); + } else { + Navigator.popUntil(context, ModalRoute.withName(AppRoutes.dashboard)); + } + } + + Future _handleBack() async { + if (_webViewController != null && await _webViewController!.canGoBack()) { + await _webViewController!.goBack(); + } else { + if (mounted) { + Navigator.pop(context); + } + } + } + + @override + Widget build(BuildContext context) { + String displayTitle = _title.isNotEmpty ? _title : LocaleKeys.offerDetails.tr(); + + return PopScope( + canPop: false, + onPopInvokedWithResult: (bool didPop, Object? result) async { + if (didPop) return; + await _handleBack(); + }, + child: Scaffold( + backgroundColor: Colors.white, + appBar: AppBarWidget( + context, + title: displayTitle, + showHomeButton: true, + onHomeTapped: _onHomeTapped, + onBackTapped: () async { + await _handleBack(); + }, + ), + body: SafeArea( + child: Stack( + children: [ + InAppWebView( + initialUrlRequest: URLRequest(url: WebUri(widget.url)), + initialSettings: InAppWebViewSettings( + javaScriptEnabled: true, + domStorageEnabled: true, + supportZoom: true, + mediaPlaybackRequiresUserGesture: false, + allowsInlineMediaPlayback: true, + javaScriptCanOpenWindowsAutomatically: true, + transparentBackground: false, + useShouldOverrideUrlLoading: false, + cacheEnabled: true, + clearCache: false, + ), + onWebViewCreated: (InAppWebViewController controller) { + _webViewController = controller; + }, + onPermissionRequest: (InAppWebViewController controller, PermissionRequest permissionRequest) async { + debugPrint("InAppWebView onPermissionRequest: ${permissionRequest.resources}"); + try { + PermissionStatus status = await Permission.camera.status; + if (!status.isGranted) { + await Permission.camera.request(); + } + } catch (e) { + debugPrint("Error requesting camera permission: $e"); + } + return PermissionResponse( + resources: permissionRequest.resources, + action: PermissionResponseAction.GRANT, + ); + }, + onProgressChanged: (InAppWebViewController controller, int progress) { + if (mounted) { + setState(() { + _progress = progress / 100; + }); + } + }, + onTitleChanged: (InAppWebViewController controller, String? title) { + if (widget.title == null || widget.title!.isEmpty) { + if (title != null && title.isNotEmpty && !title.startsWith("http")) { + if (mounted) { + setState(() { + _title = title; + }); + } + } + } + }, + ), + if (_progress < 1.0) + LinearProgressIndicator( + value: _progress, + color: const Color(0xFF168A89), + backgroundColor: Colors.transparent, + minHeight: 2.5, + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/widgets/button/default_button.dart b/lib/widgets/button/default_button.dart index ba15fad..d355a1f 100644 --- a/lib/widgets/button/default_button.dart +++ b/lib/widgets/button/default_button.dart @@ -17,11 +17,15 @@ class DefaultButton extends StatelessWidget { final Color? color; final Color? disabledColor; final IconData? iconData; + final IconData? trailingIconData; final String? svgIcon; final double? fontSize; final bool isTextExpanded; final int count; final List? colors; + final BoxBorder? border; + final double? borderRadius; + final double? height; const DefaultButton( this.text, @@ -34,8 +38,12 @@ class DefaultButton extends StatelessWidget { this.count = 0, this.textColor = Colors.white, this.iconData, + this.trailingIconData, this.fontSize, this.colors, + this.border, + this.borderRadius, + this.height, }) : super(key: key); @override @@ -43,9 +51,10 @@ class DefaultButton extends StatelessWidget { return InkWell( onTap: onPress, child: Container( - height: 43, + height: height ?? 43, decoration: BoxDecoration( - borderRadius: BorderRadius.circular(6.0), + border: border, + borderRadius: BorderRadius.circular(borderRadius ?? 6.0), gradient: onPress == null ? LinearGradient( colors: [ @@ -67,12 +76,12 @@ class DefaultButton extends StatelessWidget { child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - if (iconData != null) Icon(iconData, color: textColor), + if (iconData != null) Icon(iconData, color: textColor, size: 20), if (svgIcon != null) SvgPicture.asset(svgIcon ?? "", color: textColor), if (!isTextExpanded) Padding( - padding: EdgeInsets.only( - left: (iconData ?? svgIcon) != null ? 6 : 0, + padding: EdgeInsets.symmetric( + horizontal: (iconData ?? svgIcon ?? trailingIconData) != null ? 8 : 0, ), child: Text( text, @@ -98,6 +107,7 @@ class DefaultButton extends StatelessWidget { ), ), ), + if (trailingIconData != null) Icon(trailingIconData, color: textColor, size: 20), if (count > 0) Align( alignment: Alignment.topCenter, diff --git a/lib/widgets/dynamic_forms/dynamic_textfield_widget.dart b/lib/widgets/dynamic_forms/dynamic_textfield_widget.dart index 0630e44..4fd5fd2 100644 --- a/lib/widgets/dynamic_forms/dynamic_textfield_widget.dart +++ b/lib/widgets/dynamic_forms/dynamic_textfield_widget.dart @@ -1,11 +1,11 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:mohem_flutter_app/classes/colors.dart'; class DynamicTextFieldWidget extends StatelessWidget { final String labelText; final String hintText; - - // final TextEditingController controller; + final TextEditingController? controller; final VoidCallback? onTap; final IconData? suffixIconData; final bool isEnable; @@ -18,9 +18,13 @@ class DynamicTextFieldWidget extends StatelessWidget { final bool isObscureText; final bool isBackgroundEnable; final void Function(String)? onChange; + final int? maxLength; + final List? inputFormatters; + final Color? backgroundColor; - DynamicTextFieldWidget(this.labelText, this.hintText, //this.controller, - {this.isObscureText = false, + DynamicTextFieldWidget(this.labelText, this.hintText, + {this.controller, + this.isObscureText = false, this.onTap, this.suffixIconData, this.isEnable = true, @@ -31,7 +35,10 @@ class DynamicTextFieldWidget extends StatelessWidget { this.onChange, this.isInputTypeNum = false, this.isInputTypeNumSigned = true, - this.isBackgroundEnable = false}); + this.isBackgroundEnable = false, + this.maxLength, + this.inputFormatters, + this.backgroundColor}); @override Widget build(BuildContext context) { @@ -40,7 +47,7 @@ class DynamicTextFieldWidget extends StatelessWidget { alignment: Alignment.center, decoration: BoxDecoration( borderRadius: BorderRadius.circular(15), - color: isReadOnly ? MyColors.lightGreyEFColor : Colors.white, + color: backgroundColor ?? (isReadOnly ? MyColors.lightGreyEFColor : Colors.white), border: Border.all( color: MyColors.lightGreyEFColor, width: 1, @@ -66,6 +73,7 @@ class DynamicTextFieldWidget extends StatelessWidget { ), ), TextField( + controller: controller, enabled: isEnable, scrollPadding: EdgeInsets.zero, readOnly: isReadOnly, @@ -73,8 +81,9 @@ class DynamicTextFieldWidget extends StatelessWidget { ? (isInputTypeNumSigned ? const TextInputType.numberWithOptions(signed: true, decimal: true) : TextInputType.numberWithOptions(signed: true, decimal: true)) : TextInputType.text, textInputAction: TextInputAction.done, - //controller: controller, maxLines: lines, + maxLength: maxLength, + inputFormatters: inputFormatters, obscuringCharacter: "*", obscureText: isObscureText, onChanged: onChange, @@ -86,9 +95,10 @@ class DynamicTextFieldWidget extends StatelessWidget { letterSpacing: -0.44, ), decoration: InputDecoration( + counterText: "", isDense: true, hintText: hintText, - fillColor: isReadOnly ? MyColors.borderColor : null, + fillColor: backgroundColor ?? (isReadOnly ? MyColors.borderColor : null), hintStyle: const TextStyle( fontSize: 14, height: 21 / 14,