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
";
+ }
+
+ 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