diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 6477dfd..fa67bbc 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -4,6 +4,7 @@ + diff --git a/android/app/src/main/res/xml/filepaths.xml b/android/app/src/main/res/xml/filepaths.xml index 087acc2..4da92b3 100644 --- a/android/app/src/main/res/xml/filepaths.xml +++ b/android/app/src/main/res/xml/filepaths.xml @@ -4,5 +4,5 @@ - + \ No newline at end of file diff --git a/android/app/src/main/res/xml/network_security_config.xml b/android/app/src/main/res/xml/network_security_config.xml index f18e1f0..a3e396e 100644 --- a/android/app/src/main/res/xml/network_security_config.xml +++ b/android/app/src/main/res/xml/network_security_config.xml @@ -1,6 +1,6 @@ - + diff --git a/android/build.gradle b/android/build.gradle index 284d720..f6a880c 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -66,6 +66,20 @@ allprojects { jvmTarget = "21" } } + + if (project.name == 'nfc_manager') { + project.tasks.matching { it.name.startsWith("compile") && it.name.endsWith("Kotlin") }.configureEach { + doFirst { + def translatorFile = project.file("src/main/kotlin/io/flutter/plugins/nfcmanager/Translator.kt") + if (translatorFile.exists()) { + def content = translatorFile.text + if (content.contains("toLowerCase(Locale.ROOT)")) { + translatorFile.text = content.replace("toLowerCase(Locale.ROOT)", "lowercase(Locale.ROOT)") + } + } + } + } + } } } } diff --git a/assets/icons/duration.svg b/assets/icons/duration.svg new file mode 100644 index 0000000..ba3daa0 --- /dev/null +++ b/assets/icons/duration.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/filter_icon.svg b/assets/icons/filter_icon.svg new file mode 100644 index 0000000..72042c3 --- /dev/null +++ b/assets/icons/filter_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/ic_add_dependents.png b/assets/icons/ic_add_dependents.png new file mode 100644 index 0000000..81fd3dd 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/icons/ic_dependents_top.svg b/assets/icons/ic_dependents_top.svg new file mode 100644 index 0000000..54d92d1 --- /dev/null +++ b/assets/icons/ic_dependents_top.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/location.svg b/assets/icons/location.svg new file mode 100644 index 0000000..355aca8 --- /dev/null +++ b/assets/icons/location.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/mazaya_arrow_right.svg b/assets/icons/mazaya_arrow_right.svg new file mode 100644 index 0000000..626286f --- /dev/null +++ b/assets/icons/mazaya_arrow_right.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/mazaya_arrow_up_right.svg b/assets/icons/mazaya_arrow_up_right.svg new file mode 100644 index 0000000..d19638c --- /dev/null +++ b/assets/icons/mazaya_arrow_up_right.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/mazaya_chevron_down.svg b/assets/icons/mazaya_chevron_down.svg new file mode 100644 index 0000000..229787c --- /dev/null +++ b/assets/icons/mazaya_chevron_down.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/mazaya_chevron_left.svg b/assets/icons/mazaya_chevron_left.svg new file mode 100644 index 0000000..43a844e --- /dev/null +++ b/assets/icons/mazaya_chevron_left.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/mazaya_compass.svg b/assets/icons/mazaya_compass.svg new file mode 100644 index 0000000..804a26b --- /dev/null +++ b/assets/icons/mazaya_compass.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/icons/mazaya_heart.svg b/assets/icons/mazaya_heart.svg new file mode 100644 index 0000000..d8d9d21 --- /dev/null +++ b/assets/icons/mazaya_heart.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/mazaya_heart_outline.svg b/assets/icons/mazaya_heart_outline.svg new file mode 100644 index 0000000..3d2bd2d --- /dev/null +++ b/assets/icons/mazaya_heart_outline.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/mazaya_logo_horizontal.svg b/assets/icons/mazaya_logo_horizontal.svg new file mode 100644 index 0000000..081ebfa --- /dev/null +++ b/assets/icons/mazaya_logo_horizontal.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/icons/mazaya_logo_vertical.svg b/assets/icons/mazaya_logo_vertical.svg new file mode 100644 index 0000000..6d89751 --- /dev/null +++ b/assets/icons/mazaya_logo_vertical.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/icons/mazaya_map_pin.svg b/assets/icons/mazaya_map_pin.svg new file mode 100644 index 0000000..87e573f --- /dev/null +++ b/assets/icons/mazaya_map_pin.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/mazaya_search.svg b/assets/icons/mazaya_search.svg new file mode 100644 index 0000000..65b5a58 --- /dev/null +++ b/assets/icons/mazaya_search.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/mazaya_user_round.svg b/assets/icons/mazaya_user_round.svg new file mode 100644 index 0000000..a8619b4 --- /dev/null +++ b/assets/icons/mazaya_user_round.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/store.svg b/assets/icons/store.svg new file mode 100644 index 0000000..5cd8fdb --- /dev/null +++ b/assets/icons/store.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/images/banner_arabic.svg b/assets/images/banner_arabic.svg new file mode 100644 index 0000000..d7e8264 --- /dev/null +++ b/assets/images/banner_arabic.svg @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/images/banner_english.svg b/assets/images/banner_english.svg new file mode 100644 index 0000000..8a8f058 --- /dev/null +++ b/assets/images/banner_english.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/images/cat_all.png b/assets/images/cat_all.png new file mode 100644 index 0000000..3bc5554 Binary files /dev/null and b/assets/images/cat_all.png differ diff --git a/assets/images/cat_fitness.png b/assets/images/cat_fitness.png new file mode 100644 index 0000000..e080708 Binary files /dev/null and b/assets/images/cat_fitness.png differ diff --git a/assets/images/cat_food.png b/assets/images/cat_food.png new file mode 100644 index 0000000..a70fc4d Binary files /dev/null and b/assets/images/cat_food.png differ diff --git a/assets/images/cat_retail.png b/assets/images/cat_retail.png new file mode 100644 index 0000000..59a2f0d Binary files /dev/null and b/assets/images/cat_retail.png differ diff --git a/assets/images/logos/hmg_mohem_logo.png b/assets/images/logos/hmg_mohem_logo.png new file mode 100644 index 0000000..4b8870e Binary files /dev/null and b/assets/images/logos/hmg_mohem_logo.png differ diff --git a/assets/images/logos/mohem_logo.png b/assets/images/logos/mohem_logo.png new file mode 100644 index 0000000..4745144 Binary files /dev/null and b/assets/images/logos/mohem_logo.png differ diff --git a/assets/images/mazaya.png b/assets/images/mazaya.png new file mode 100644 index 0000000..b750724 Binary files /dev/null and b/assets/images/mazaya.png differ diff --git a/assets/images/mazaya_banner.svg b/assets/images/mazaya_banner.svg new file mode 100644 index 0000000..8a8f058 --- /dev/null +++ b/assets/images/mazaya_banner.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/images/mazaya_banner_ar.svg b/assets/images/mazaya_banner_ar.svg new file mode 100644 index 0000000..d7e8264 --- /dev/null +++ b/assets/images/mazaya_banner_ar.svg @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/images/mazaya_banner_en.svg b/assets/images/mazaya_banner_en.svg new file mode 100644 index 0000000..8a8f058 --- /dev/null +++ b/assets/images/mazaya_banner_en.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/images/mohem.png b/assets/images/mohem.png new file mode 100644 index 0000000..8997e7c Binary files /dev/null and b/assets/images/mohem.png differ diff --git a/assets/langs/ar-SA.json b/assets/langs/ar-SA.json index eebe449..b567229 100644 --- a/assets/langs/ar-SA.json +++ b/assets/langs/ar-SA.json @@ -1,47 +1,47 @@ { - "mohemm": "Mohemm", + "mohemm": "MoheM", "english": "English", "arabic": "عربي", "login": "تسجيل الدخول", "pleaseEnterLoginDetails": "الرجاء إدخال التفاصيل أدناه لتسجيل الدخول", - "userId": "UserID", + "userId": "معرّف المستخدم", "username": "اسم المستخدم", "userName": "اسم المستخدم", - "userSearch": "User Search", + "userSearch": "البحث عن مستخدم", "password": "كلمة المرور", "welcomeBack": "مرحبا بعودتك", "wouldYouLikeToLoginWithCurrentUsername": "هل ترغب في تسجيل الدخول باسم المستخدم الحالي؟", "lastLoginDetails": "تفاصيل تسجيل الدخول الأخير:", "verificationType": "نوع التحقق:", - "pleaseVerify": "ارجوك تحقق", + "pleaseVerify": "يرجى التحقق", "pleaseVerifyForBio": "الرجاء التحقق من تسجيل الدخول باستخدام أحد هذه الخيارات", - "verifyThroughFace": "تحقق من خلال الوجه", - "verifyThroughFingerprint": "تحقق من خلال بصمة الإصبع", - "verifyThroughSMS": "تحقق من خلال الرسائل القصيرة", - "verifyThroughWhatsapp": "تحقق من خلال Whatsapp", + "verifyThroughFace": "التحقق بالتعرف على الوجه", + "verifyThroughFingerprint": "التحقق ببصمة الإصبع", + "verifyThroughSMS": "التحقق عبر رسالة نصية", + "verifyThroughWhatsapp": "التحقق عبر واتساب", "useAnotherAccount": "استخدم حسابا آخر", "pleaseEnterTheVerificationCodeSentTo": "الرجاء إدخال رمز التحقق المرسل إلى ", "theVerificationCodeWillExpireIn": "ستنتهي صلاحية رمز التحقق في ", "goodMorning": "صباح الخير", - "markAttendance": "علامة الحضور", + "markAttendance": "تسجيل الحضور", "timeLeftToday": "الوقت المتبقي اليوم", - "checkIn": "تحقق في", + "checkIn": "تسجيل الحضور", "workList": "قائمة العمل", - "leaveBalance": "رصيد الاجازات", - "missingSwipes": "تسجيل بصمة حضور", - "ticketBalance": "رصيد التذكرة", + "leaveBalance": "رصيد الإجازات", + "missingSwipes": "البصمات المفقودة", + "ticketBalance": "رصيد التذاكر", "other": "آخر", "services": "خدمات", "viewAllServices": "عرض جميع الخدمات", "monthlyAttendance": "الحضور الشهري", - "vacationRule": "قاعدة الاجازات", - "vacationType": "نوع الاجازة", + "vacationRule": "قواعد الإجازات", + "vacationType": "نوع الإجازة", "startDateT": "تاريخ البدء", "endDateT": "تاريخ الانتهاء", "workFromHome": "العمل من المنزل", "ticketRequest": "طلب تذكرة", "viewAllOffers": "مشاهدة جميع العروض", - "offers": "عروض & ", + "offers": "العروض و", "discounts": "الخصومات", "newString": "جديد", "setTheNewPassword": "قم بتعيين كلمة المرور الجديدة", @@ -53,115 +53,115 @@ "home": "الرئيسية", "mySalary": "راتبي", "createRequest": "إنشاء طلب", - "forgotPassword": "هل نسيت كلمة السر", - "employeeId": "هوية الموظف", - "loginCodeWillSentToMobileNumber": "الرجاء إدخال معرف الموظف الخاص بك ، وسيتم إرسال رمز تسجيل الدخول إلى رقم هاتفك المحمول", + "forgotPassword": "هل نسيت كلمة المرور؟", + "employeeId": "الرقم الوظيفي", + "loginCodeWillSentToMobileNumber": "الرجاء إدخال رقم الموظف الخاص بك ، وسيتم إرسال رمز تسجيل الدخول إلى رقم هاتفك المسجل", "changePassword": "تغيير كلمة المرور", "ok": "موافق", - "confirm": "تؤكد", - "passwordChangedSuccessfully": "تم تغيير الرقم السري بنجاح", + "confirm": "تأكيد", + "passwordChangedSuccessfully": "تم تغيير كلمة المرور بنجاح", "itemsForSale": "سلع للبيع", "attendanceDetails": "تفاصيل الحضور", "order": "الطلبات", - "earlyOut": "الخروج مبكرا", - "shortage": "ساعات التقصير", - "excess": "فائض", - "lateIn": "القدوم المتاخر", - "approvedCheckOut": "اعتماد وقت الخروج", - "approvedCheckIn": "اعتماد وقت الدخول", - "actualCheckOut": "وقت الخروج", - "actualCheckIn": "وقت الدخول", + "earlyOut": "الانصراف المبكر", + "shortage": "ساعات غير مغطاة", + "excess": "ساعات العمل الإضافية", + "lateIn": "التأخر عن الحضور", + "approvedCheckOut": "وقت الانصراف المعتمد", + "approvedCheckIn": "وقت الحضور المعتمد", + "actualCheckOut": "وقت الانصراف الفعلي", + "actualCheckIn": "وقت الحضور الفعلي", "present": "حضور", "pres": "حضور", - "shiftTime": "وقت التناوب", + "shiftTime": "وقت المناوبة", "absent": "غياب", "attendance": "الحضور", - "scheduleDays": "ايام العمل", - "offDays": "ايام الراحه", - "nonAnalyzed": "لايوجد تحليل", - "shortageHour": "ساعات التقصير", - "stats": "الحاله", - "completed": "تم اكمال", - "msg": "Hello {} in the {} world ", - "msg_named": "{} are written in the {lang} language", - "clickMe": "Click me", - "doNotUseRecentPassword": "لا تستخدم كلمة مرور حديثة", + "scheduleDays": "أيام العمل", + "offDays": "أيام الراحة", + "nonAnalyzed": "لم يتم التحليل", + "shortageHour": "ساعات غير مغطاة", + "stats": "الإحصاءات", + "completed": "مكتمل", + "msg": "مرحبًا {} في عالم {}", + "msg_named": "{} مكتوبة بلغة {lang}", + "clickMe": "اضغط هنا", + "doNotUseRecentPassword": "لا تستخدم كلمة مرور سبق استخدامها مؤخرًا", "atLeastOneLowercase": "حرف صغير واحد على الأقل", "atLeastOneUppercase": "حرف كبير واحد على الأقل", "atLeastOneNumeric": "رقم واحد على الأقل", "minimum8Characters": "8 أحرف على الأقل", "doNotAddRepeatingLetters": "لا تقم بإضافة أحرف متكررة", - "itShouldContainSpecialCharacter": "يجب لا يحتوي على أحرف خاصة", - "confirmPasswordMustMatch": "يجب أن يتطابق تأكيد كلمة المرور", + "itShouldContainSpecialCharacter": "يجب أن تحتوي كلمة المرور على رمز خاص", + "confirmPasswordMustMatch": "يجب أن تتطابق كلمتا المرور", "sms": "رسالة قصيرة", "fingerPrint": "بصمة", "face": "التعرف على الوجه", "whatsapp": "واتس اب", - "reject": "يرفض", - "approve": "يوافق", + "reject": "رفض", + "approve": "موافقة", "cancel": "إلغاء", - "generate": "يولد", + "generate": "إنشاء", "requestedItems": "العناصر المطلوبة", "request": "طلب", - "myRequest": "طلبي", - "actions": "أجراءات", - "delegate": "مندوب", - "request_info": "اطلب معلومات", + "myRequest": "طلباتي", + "actions": "الإجراءات", + "delegate": "تفويض", + "request_info": "طلب معلومات إضافية", "attachments": "المرفقات", "info": "معلومات", "employeeNumber": "رقم الموظف", - "assignmentNumber": "رقم الواجب", + "assignmentNumber": "رقم التكليف", "employeeName": "اسم الموظف", - "scheduleDate": "تاريخ الجدول الزمني", - "shiftType": "نوع التحول", - "shift": "يحول", + "scheduleDate": "تاريخ الجدول", + "shiftType": "نوع المناوبة", + "shift": " المناوبة", "breakText": "استراحة", - "actualSwipeStart": "بدء التمرير الفعلي", - "actualSwipeEnd": "التمرير الفعلي للنهاية", - "approvedSwipeStart": "وافق انتقاد البدء", - "approvedSwipeStartReason": "تمت الموافقة على سبب بدء التمرير السريع", - "approvedSwipeEnd": "تمت الموافقة على تمرير النهاية", - "approvedSwipeEndReason": "الموافقة على سبب إنهاء التمرير", + "actualSwipeStart": "وقت تسجيل الحضور", + "actualSwipeEnd": "وقت تسجيل المغادرة", + "approvedSwipeStart": "وقت الحضور المعتمد", + "approvedSwipeStartReason": "سبب تعديل وقت الحضور المعتمد", + "approvedSwipeEnd": "وقت الانصراف المعتمد", + "approvedSwipeEndReason": "سبب تعديل وقت الانصراف المعتمد", "from": "من", - "to": "ل", - "sent": "أرسلت", + "to": "إلى", + "sent": "تم الإرسال", "closed": "مغلق", - "id": "هوية شخصية", + "id": "المعرّف", "responder": "المستجيب", - "jobTitle": "عنوان وظيفي", - "grade": "درجة", + "jobTitle": "المسمى الوظيفي", + "grade": "الدرجة الوظيفية", "jobCategory": "تصنيف الوظيفة", "category": "فئة", "employeeEmailAddress": "عنوان البريد الإلكتروني للموظف", "payrollBranch": "فرع الرواتب", "yourChangeHasBeenSavedSuccessfully": "تم حفظ التغيير الخاص بك بنجاح", - "code": "شفرة", + "code": "الرمز", "unit": "وحدة", "quantity": "كمية", - "dateRequired": "التاريخ مطلوب", - "lineStatus": "حالة الخط", + "dateRequired": "التاريخ المطلوب", + "lineStatus": "حالة البند", "statusDate": "تاريخ الحالة", "transactionType": "نوع المعاملة", - "operatingUnit": "وحدة التشغيل", - "organizationCode": "كود المنظمة", - "organization": "منظمة", - "fromSubInventory": "من الجرد الفرعي", - "fromLocator": "من محدد المواقع", - "toSubInventory": "إلى الجرد الفرعي", - "toLocator": "إلى محدد المواقع", - "shipToLocator": "شحن إلى محدد المواقع", - "itemHistory": "تاريخ العنصر", - "mfg": "مبدع", - "lineType": "نوع الخط", + "operatingUnit": "الوحدة التشغيلية", + "organizationCode": "رمز المنشأة", + "organization": "المنشأة", + "fromSubInventory": "من المخزون الفرعي", + "fromLocator": "من موقع التخزين", + "toSubInventory": "إلى المخزون الفرعي", + "toLocator": "إلى موقع التخزين", + "shipToLocator": "موقع التخزين المستلم", + "itemHistory": "سجل الصنف", + "mfg": "الشركة المصنّعة", + "lineType": "نوع البند", "price": "السعر", - "lineAmount": "مبلغ الخط", - "lineDiscount": "خصم الخط٪", - "needByDate": "القادمة إلى الأمام", - "promisedDate": "التسجيل وعد", + "lineAmount": "قيمة البند", + "lineDiscount": "خصم البند (%)", + "needByDate": "تاريخ الحاجة", + "promisedDate": "تاريخ التسليم المتعهد به", "deliverToLocation": "تسليم إلى الموقع", "requisitionNumber": "رقم الطلب", "requester": "مقدم الطلب", - "quotationAnalysis": "تحليل الاقتباس", + "quotationAnalysis": "تحليل عروض الأسعار", "subject": "موضوعات", "description": "وصف", "pleaseEnterDescription": "الرجاء إدخال الوصف", @@ -171,65 +171,65 @@ "preparer": "معد", "creationDate": "تاريخ الإنشاء", "shipToLocation": "الشحن الى الموقع", - "quotationNumber": "رقم الإقتباس", - "quotationDate": "تاريخ الاقتباس", + "quotationNumber": "رقم عرض السعر", + "quotationDate": "تاريخ عرض السعر", "paymentTerms": "شروط الدفع", "currency": "عملة", - "grossAmount": "المبلغ الإجمالي", - "discountAmount": "مقدار الخصم", + "grossAmount": "إجمالي المبلغ", + "discountAmount": "مبلغ الخصم", "customDuty": "الرسوم الجمركية", - "shipHandle": "مقبض السفينة", + "shipHandle": "رسوم الشحن والمناولة", "otherCharges": "رسوم أخرى", "totalPOAmountWithVAT": "إجمالي مبلغ الشراء مع ضريبة القيمة المضافة", - "totalPOAmountInWords": "إجمالي مبلغ أمر الشراء بالكلمات", + "totalPOAmountInWords": "إجمالي قيمة أمر الشراء كتابةً", "requestNumber": "رقم الطلب", - "uom": "UOM", + "uom": "وحدة القياس", "operatingCode": "كود التشغيل", - "poNumber": "PO عدد", + "poNumber": "رقم أمر الشراء", "revision": "مراجعة", "quantityOrdered": "الكمية المطلوبة", "quantityReceived": "الكمية المستلمة", - "bonusQuantity": "كمية المكافأة", + "bonusQuantity": "الكمية المجانية", "purchasePrice": "سعر الشراء", "discountPer": "خصم ٪", - "balanceQuantity": "كمية التوازن", + "balanceQuantity": "الكمية المتبقية", "netPrice": "السعر الصافي", "closureStatus": "حالة الإغلاق", - "quotationNetPrice": "صافي سعر الاقتباس", - "quotationUOM": "اقتباس UOM", - "quotationQty": "اقتباس الكمية", + "quotationNetPrice": "صافي سعر عرض السعر", + "quotationUOM": "وحدة القياس في عرض السعر", + "quotationQty": "الكمية في عرض السعر", "itemCode": "رمز الصنف", "vendorName": "اسم البائع", - "quotationMFGPartNumber": "رقم الجزء MFG الاقتباس", + "quotationMFGPartNumber": "رقم قطعة الشركة المصنّعة في عرض السعر", "quotationDeliveryDate": "تاريخ تسليم عرض الأسعار", - "quotationBonusQuantity": "كمية مكافأة الاقتباس", - "quotationLineTotal": "مجموع خط الاقتباس", - "rfqUOM": "RFQ UOM", - "rfqQty": "RFQ الكمية", - "rfqNumber": "رقم RFQ", - "human": "بشري", - "resources": "موارد", + "quotationBonusQuantity": "الكمية المجانية في عرض السعر", + "quotationLineTotal": "إجمالي بند عرض السعر", + "rfqUOM": "وحدة القياس لطلب عرض السعر", + "rfqQty": "الكمية المطلوبة لعرض السعر", + "rfqNumber": "رقم طلب عرض السعر", + "human": "الموارد", + "resources": "البشرية", "details": "تفاصيل", "noDataAvailable": "لا تتوافر بيانات", "productName": "اسم المنتج", "productDescription": "وصف المنتج", - "unitPrice": "سعر الوحده", + "unitPrice": "سعر الوحدة", "manufacturerName": "اسم المصنع", - "manufacturerPartName": "اسم جزء الشركة المصنعة", + "manufacturerPartName": "اسم قطعة الشركة المصنّعة", "supplierName": "اسم المورد", - "supplierContact": "الاتصال بالمورد", - "chargeToPatient": "المسؤول عن المريض", + "supplierContact": "بيانات التواصل مع المورد", + "chargeToPatient": "يُحمّل على حساب المريض", "justification": "التبرير", "itemDescription": "وصف السلعة", "groupCode": "كود المجموعة", - "primaryUOM": "UOM الابتدائية", + "primaryUOM": "وحدة القياس الأساسية", "subgroupDescription": "وصف المجموعة الفرعية", "subgroupCode": "رمز المجموعة الفرعية", "groupDescription": "وصف المجموعة", "templateName": "اسم القالب", - "itemCreationStatus": "حالة إنشاء العنصر", - "standardizationApprovalStatus": "حالة الموافقة على التقييس", - "standardizationApprovalRejectionReason": "سبب رفض الموافقة على التقييس", + "itemCreationStatus": "حالة إنشاء الصنف", + "standardizationApprovalStatus": "حالة اعتماد التوحيد القياسي", + "standardizationApprovalRejectionReason": "سبب رفض اعتماد التوحيد القياسي", "analyzedBy": "تحليل بواسطة", "approvedDate": "تاريخ الموافقة", "itemType": "نوع العنصر", @@ -237,38 +237,38 @@ "requestDate": "تاريخ الطلب", "analyzedDate": "تاريخ التحليل", "urgent": "العاجلة", - "requestDetails": "طلب تفاصيل", + "requestDetails": "تفاصيل الطلب", "approvalLevel": "مستوى الموافقة", "requesterDetails": "تفاصيل مقدم الطلب", "myAttendance": "حضوري", - "workOnBreak": "التعويض عن العمل اثناءالاستراحه", + "workOnBreak": "التعويض عن العمل أثناء الاستراحة", "next": "التالي", - "apply": "يتقدم", - "mobile": "التليفون المحمول", - "completingYear": "نحن نقدر لك لاستكمال خدمة", + "apply": "تقديم الطلب", + "mobile": "الجوال", + "completingYear": "نقدّر لك إتمام مدة خدمة", "year": "سنة", "month": "شهر", "day": "يوم", "address": "العنوان", "phoneNumber": "رقم الجوال", "businessGroup": "مجموعة العمل", - "Payroll": "الراتب", + "Payroll": "الرواتب", "civilIdentityNumber": "رقم الهويه", "dateOfBirth": "تاريخ الميلاد", "maritalStatus ": "الحالة الاجتماعية", - "fullName": "الأسم الكامل", + "fullName": "الاسم الكامل", "remove": "حذف", - "submit": "ارسال", - "submitAsAnonymous": "إرسال كمجهول", - "areYouSureYouWantToSubmit": "هل أنت متأكد أنك تريد أن تقدم؟", + "submit": "إرسال", + "submitAsAnonymous": "إرسال دون الإفصاح عن الهوية", + "areYouSureYouWantToSubmit": "هل أنت متأكد من إرسال الطلب؟", "comments": "تعليقات", "writeComment": "أكتب تعليقا", - "approversList": "قائمة الموافقين", - "yourRequestHasBeenSubmittedForApprovals": "تم تقديم طلبك للموافقات", + "approversList": "قائمة المعتمدين", + "yourRequestHasBeenSubmittedForApprovals": "تم إرسال طلبك للاعتماد", "monthlyPaySlip": "قسيمة الراتب الشهرية", - "particular": "خاص", - "earnings": "أرباح", - "deductions": "الخصومات", + "particular": "البيان", + "earnings": "المستحقات", + "deductions": "الاستقطاعات", "paymentMethodName": "اسم طريقة الدفع", "bankName": "اسم البنك", "branchCode": "رمز الفرع", @@ -276,28 +276,28 @@ "summaryOfInformation": "ملخص المعلومات", "totalPayAmount": "المبلغ الإجمالي للدفع", "paymentInformation": "معلومات الدفع", - "performance": "تقييم الاداء", + "performance": "تقييم الأداء", "performanceEvaluation": "تقييم الأداء في", "performanceEvaluationIn": "تقييم أدائك في", "valuationIn": "تقييم الأداء في", - "amount": "مقدار", - "correctCurrentDatails": "تعديل او اكمال التفاصيل الحالية", + "amount": "المبلغ", + "correctCurrentDatails": "تصحيح البيانات الحالية أو استكمالها", "selectType": " حدد نوع التغيير الذي تريد القيام به", "enterNewInfo": " أدخل معلومات جديدة بسبب تغيير حقيقي في التفاصيل الحالية (على سبيل المثال بسبب تغيير في الحالة الاجتماعية", "endDate": "تاريخ الانتهاء", - "removeThisMember": "هل انت متأكد تريد ازالة هذا العضو؟", - "wantUpdateThisMember ": "هل انت متأكد تريد تحديث بيانات هذا العضو؟", + "removeThisMember": "هل أنت متأكد من إزالة هذا الفرد؟", + "wantUpdateThisMember ": "هل أنت متأكد من تحديث بيانات هذا الفرد؟", "addNewFamilyMember": "اضافة عضو جديد", "addRow": "اضافة صف جديد", "pleaseSelect": "الرجاء اختيار", "delete": "حذف", "edit": "تعديل", "add": "اضافه", - "myProfile": "معلوماتي", + "myProfile": "ملفي الشخصي", "mowadhafhi": "موظفي", - "searchAnnouncements": "بحث الاعلانات", + "searchAnnouncements": "البحث في الإعلانات", "announcements": "اعلانات", - "swipeRequest": "طلب تسجيل حضور", + "swipeRequest": "طلب تصحيح البصمة", "serviceType": "نوع الخدمه", "departmentName": "اسم القسم", "selectDepartment": "اختر القسم", @@ -307,47 +307,47 @@ "selectTopic": "اختر العنوان", "supportingDocument": "ارفاق مستند", "mowadhafhiRequest": "طلب موظفي", - "ticketReference": "مرجع التذكره", + "ticketReference": "الرقم المرجعي للتذكرة", "section": "القسم", "topic": "العنوان", - "actionBy": "الرد بواسطة", - "pending": "معلق", - "pendingTransactions": "المعاملات المعلقه", + "actionBy": "الإجراء بواسطة", + "pending": "قيد الانتظار", + "pendingTransactions": "المعاملات قيد الانتظار", "selectRequestType": "الرجاء اختيار نوع الطلب", "dateFrom": "من تاريخ", "dateTo": "الى تاريخ", "requestName": "اسم الطلب", - "createdFor": "انشاء لأجل", + "createdFor": "تم الإنشاء لصالح", "requestCreatedSuccessfully": "تم انشاء الطلب بنجاح", "search": "بحث", "filterEnterPrDetailsHere": "ادخل تفاصيل طلب الشراء هنا", "filterEnterPoDetailsHere": "ادخل تفاصيل أمر الشراء هنا", - "wantToReject": "هل انت متأكد تريد الرفض", + "wantToReject": "هل أنت متأكد من رفض الطلب؟", "requestType": "نوع الطلب", - "employeeDigitalID": "هويةالموظف الرقمية", + "employeeDigitalID": "الهوية الرقمية للموظف", "businessCard": "بطاقة العمل", "viewBusinessCard": "عرض بطاقة العمل", "logout": "تسجيل خروج", - "checkOut": "وقت الخروج", - "regular": "منتظم", - "mark": "علامة", + "checkOut": "تسجيل الانصراف", + "regular": "اعتيادي", + "mark": "تسجيل", "selectMethodOfAttendance": "اختر طريقة تسجيل الحضور", - "comeNearHMGWifi": "HMG wifi من فضلك اقترب من", + "comeNearHMGWifi": "يرجى الاقتراب من شبكة Wi-Fi الخاصة بالمجموعة", "deliverNotificationToMeRegardless": "تسليم الإخطارات إلي بغض النظر عن أي قواعد عامة", "close": "أغلق", - "respond": "يرد", + "respond": "رد", "vacationRuleAdded": "تمت إضافة قاعدة الإجازة", "selectTypeT": "اختر صنف", "notification": "تنبيه", "selectNotification": "حدد إعلام", "ifAllSelectedYouWillSkip": "* إذا تم تحديد الكل ، فستنتقل إلى الخطوة 3", - "applyForVacationRule": "التقدم بطلب للحصول على قانون الإجازة", + "applyForVacationRule": "طلب تطبيق قواعد الإجازات", "step1": "الخطوة 1", "step2": "الخطوة 2", "step3": "الخطوه 3", "message": "رسالة", "writeAMessage": "اكتب رسالة", - "notificationReassign": "إعادة تعيين الإخطار", + "notificationReassign": "إعادة توجيه الإشعار", "selectEmployee": "حدد الموظف", "searchEmployeeForReplacement": "ابحث عن موظف بديل", "searchForEmployee": "ابحث عن موظف", @@ -358,47 +358,47 @@ "pleaseSelectDate": "الرجاء تحديد التاريخ", "todayAttendance": "حضور اليوم", "viewAttendance": "عرض الحضور", - "teamMembers": "اعضاءالفريق", + "teamMembers": "أعضاء الفريق", "profileDetails": "الملف الشخصي", - "noResultsFound": "لايوجد نتائج", + "noResultsFound": "لم يتم العثور على نتائج", "searchBy": "بحث بواسطة", - "myTeamMembers": "اعضاء فريقي", + "myTeamMembers": "أعضاء فريقي", "save": "حفظ", "TurnNotificationsFor": "تفعيل الاشعارات", "worklistSettings": "اعدادات الاشعارات", "absenceType": "نوع الغياب", - "absenceCategory": "فئة الغياب", + "absenceCategory": "تصنيف الغياب", "days": "أيام", "hours": "ساعات", - "approvalStatus": "حالة القبول", + "approvalStatus": "حالة الموافقة", "absenceStatus": "حالة الغياب", - "subordinateLeave": "إجازة التابعيين", + "subordinateLeave": "إجازات الموظفين التابعين", "numberDays": "عدد الأيام", - "poweredBy": "مشغل بواسطة", - "cloudSolutions": "حلول السحابة", + "poweredBy": "بدعم من", + "cloudSolutions": "الحلول السحابية", "selectTemplate": "حدد قالب", "myPostedAds": "إعلاناتي المنشورة", "browseCategories": "تصفح الفئات", "searchItems": "عناصر البحث", "offerAndDiscounts": "العروض والخصومات", - "offerValid": "العرض صالح", + "offerValid": "العرض ساري", "offerExpired": "انتهى العرض", "whatAreYouOffering": "ما الذي تعرضه؟", "selectCategory": "اختر الفئة", - "inProgress": "في تَقَدم", + "inProgress": "قيد التنفيذ", "locked": "مقفل", "addDetails": "أضف التفاصيل", - "reviewAndSell": "مراجعة وبيع", + "reviewAndSell": "المراجعة والنشر للبيع", "itemTitle": "عنوان البند", "itemCondition": "حالة السلعة", - "used": "تستخدم", + "used": "مستعمل", "region": "منطقة", "selectRegion": "اختر المنطقة", "itemPrice": "سعر السلعة", - "itemPhotos": "صور البند", + "itemPhotos": "صور السلعة", "itemInfo": "معلومات العنصر", "uploadAttachment": "تحميل المرفق", - "selectFromGalleryOrOpenCamera": "اختر من المعرض أو فتح الكاميرا", + "selectFromGalleryOrOpenCamera": "اختر صورة من المعرض أو التقط صورة بالكاميرا", "openCamera": "فتح\nالكاميرا", "uploadFromGallery": "تحميل من\nملفات الجهاز", "name": "الأسم", @@ -406,22 +406,22 @@ "noHistoryAvailable": "لايوجد سجل بيانات سابقة ", "purchaseRequisition": "طلب شراء", "moveOrder": "طلب تغيير", - "humanResource": "الموارد البشريه", - "purchaseOrder": "امر شراء", + "humanResource": "الموارد البشرية", + "purchaseOrder": "أمر شراء", "ITGForms": "ITG نماذج", - "itemCreation": "أنشاء عنصر", + "itemCreation": "إنشاء صنف", "stamp": "ختم", "addFavoriteList": "هل تريد اضافة {name} لقائمة المفضله", "feedbackUserExperience": "هذا للحصول على تعليقات حول تجربة المستخدم", - "rateUI": "كيف تريد أن تقيم", - "rateUI2": "ما مدى رضائك عن هذا التطبيق", + "rateUI": "كيف تقيّم واجهة التطبيق؟", + "rateUI2": "ما مدى رضاك عن هذا التطبيق؟", "submitSurvey": "ارسال الاستبيان", "typeHere": "اكتب هنا", "infoDetail": "تفاصيل المعلومات", "amount_detail": "تفاصيل المبلغ", "currentBalance": "الرصيد الحالي", - "currentLeaveBalance": "رصيد الاجازات الحالي", - "calculatedDays": "الايام المحسوبه", + "currentLeaveBalance": "رصيد الإجازات الحالي", + "calculatedDays": "الأيام المحتسبة", "totalDays": "مجموع الأيام", "usedBalance": "المستخدم", "infants": "رضيع", @@ -439,18 +439,18 @@ "SelectChangeWantToMake": " حدد نوع التغيير الذي تريد القيام به.", "profile": { "reset_password": { - "label": "Reset Password", - "username": "Username", - "password": "password" + "label": "إعادة تعيين كلمة المرور", + "username": "اسم المستخدم", + "password": "كلمة المرور" }, "profileCompletionPer": "استكمال الملف الشخصي", - "completeProfile": "الملف الشخصي الكامل", + "completeProfile": "استكمال الملف الشخصي", "personalInformation": "معلومات شخصية", "basicDetails": "تفاصيل أساسية", "address": "العنوان", "contactDetails": "بيانات التواصل", "familyDetails": "تفاصيل عائلية", - "effectiveDate": "تاريخ النفاذ", + "effectiveDate": "تاريخ سريان التغيير", "country": "دولة" }, "clicked": { @@ -473,11 +473,11 @@ "chat": "دردشة", "mychats": "دردشاتي", "advancedSearch": "بحث متقدم", - "openNot": "التبليغات المفتوحة", - "fyi": "تبليغات للعلم", - "toDo": "تبليغات الأعمال", - "all": "كل التبليغات", - "meNot": "تبليغات صادرة مني", + "openNot": "الإشعارات المفتوحة", + "fyi": "إشعارات للاطلاع", + "toDo": "إشعارات تتطلب إجراء", + "all": "جميع الإشعارات", + "meNot": "الإشعارات الصادرة مني", "view": "عرض", "fromUserName": "من", "sentDate": "تاريخ الإرسال", @@ -499,15 +499,15 @@ "demoMarathonNoteP3": "قبل الوقت الفعلي.", "sponsoredBy": "برعاية:", "question": "سؤال", - "marathoners": "الماراثون", - "marathoner": "ماراثونر", + "marathoners": "المشاركون في الماراثون", + "marathoner": "مشارك في الماراثون", "prize": "جائزة:", "winnerSelection": "اختيار الفائز", "qualifiers": "تصفيات", "qualifier": "المؤهل", "getReadyForContest": "استعد للمسابقة القادمة:", - "winnerSelectedRandomly": "سيتم اختيار الفائز عشوائياً من بين التصفيات.", - "fingersCrossed": "تشابك الاصابع!!!", + "winnerSelectedRandomly": "سيتم اختيار الفائز عشوائيًا من بين المتأهلين.", + "fingersCrossed": "نتمنى لك التوفيق!", "congrats": "مبروك !!!", "allQuestionsCorrect": "لقد أجبت على جميع الأسئلة بشكل صحيح.", "otp": "OTP", @@ -526,19 +526,19 @@ "ourSponsor": "راعينا:", "startingIn": "يبدأ في", "youAreOutOfContest": "أنت خارج المسابقة.", - "winners": "الفائزين!!!", + "winners": "الفائزون", "expireAfter": "تنتهي بعد", - "oneWeek": "أسبوع 1", - "twoWeek": "2 أسبوع", - "noUpcoming": "لا يوجد قادم", - "fakeLocation": ".لقد تتبعنا أنك تحاول استخدام موقع مزيف! يعتبر هذا مخالفة وقد تم إخطار الموارد البشرية", - "noWinner": "حزين! لم يفز أحد اليوم.", + "oneWeek": "أسبوع واحد", + "twoWeek": "أسبوعان", + "noUpcoming": "لا توجد مسابقات قادمة", + "fakeLocation": "تم رصد محاولة استخدام موقع جغرافي غير حقيقي. يُعد ذلك مخالفة، وقد تم إشعار إدارة الموارد البشرية.", + "noWinner": "لم يفز أحد اليوم.", "myTeam": "فريقي", "youCanPlayDemo": "لكن يمكنك لعب العرض", "group": "مجموعة", - "searchGroup": "مجموعة البحث", + "searchGroup": "البحث عن مجموعة", "connectHmgWifi": "قم بتوصيل HMG WIFI", - "connectedHmgWifi": "اتصال HMG WIFI", + "connectedHmgWifi": "متصل بشبكة HMG Wi-Fi", "itgForms": "نماذج (ITG)", "resetAdPassword": "إعادة تعيين كلمة مرور AD", "myDocuments": "مستنداتي", @@ -551,60 +551,66 @@ "addAtLeastOneAttachment": "الرجاء إضافة مرفق واحد على الأقل.", "pleaseClickButtonToJoinMarathon": "الرجاء الضغط على الزر أدناه للانضمام إلى الماراثون", "youCannotJoinTheMarathon": "لا يمكنك الانضمام إلى الماراثون لأنك تجاوزت الحد الزمني", - "open": "يفتح", - "youCannotJoinTheMarathon": "لا يمكنك الانضمام إلى الماراثون لأنك تجاوزت الحد الزمني", - "pleaseClickButtonToJoinMarathon": "الرجاء الضغط على الزر أدناه للانضمام إلى الماراثون", - "generate": "يولد", + "open": "فتح", "paymentRequest": "طلب الدفع", "paymentDetails": "تفاصيل الدفع", "requestNo": "رقم الطلب", "requesterEmpNum": "رقم الموظف الذي قدم الطلب", "requesterEmpName": "اسم الموظف مقدم الطلب", - "prepareEmpName": "إعداد اسم الموظف", + "prepareEmpName": "اسم الموظف المُعدّ للطلب", "requesterPositionName": "اسم الوظيفة لمقدم الطلب", - "preparePositionName": "إعداد اسم الوظيفة", + "preparePositionName": "المسمى الوظيفي لمُعدّ الطلب", "requesterPayrollName": "اسم مقدم الطلب", - "payingORGName": "اسم المنظمة الدافعة", + "payingORGName": "اسم الجهة الدافعة", "requestAmount": "مبلغ الطلب", "typeofPayment": "نوع الدفع", "beneficiaryDetails": "تفاصيل المستفيد", "beneficiaryName": "اسم المستفيد", - "idIqama": "الهوية السعودية / الإقامة", - "beneficiaryBankName": "اسم البنك المستفيد", + "idIqama": "رقم الهوية الوطنية أو الإقامة", + "beneficiaryBankName": "بنك المستفيد", "sadadNumber": "رقم سداد", "beneficiaryIBAN": "رقم IBAN للمستفيد", - "purchaseOrders": "طلبات الشراء", + "purchaseOrders": "أوامر الشراء", "approvalDate": "تاريخ الموافقة", "pOAmount": "مبلغ الطلب", "versionStatus": "حالة الإصدار", "supplierNo": "رقم المورد", "general": "عام", "requesterOperatingUnit": "وحدة تشغيل مقدم الطلب", - "prepareEmpNum": "إعداد رقم الموظف", + "prepareEmpNum": "الرقم الوظيفي لمُعدّ الطلب", "supplierInfo": "معلومات المورد", "supplierAcNo": "رقم حساب المورد", "supplierAcName": "اسم حساب المورد", "supplierIBAN": "رقم IBAN للمورد", "supplierCRNo": "رقم السجل التجاري", "suppliedAcNo": "رقم الحساب المقدم", - "patientRefundInvoice": "فاتورة استرداد الأموال للمريض", + "patientRefundInvoice": "فاتورة استرداد مبلغ للمريض", "patientNumber": "رقم المريض", "patientName": "اسم المريض", "invoiceDate": "تاريخ الفاتورة", - "refundInvoice": "فاتورة الاسترجاع", - "hospitalClinic": "عيادة المستشفى", - "graphicalAnalysis": "التحليل الرسومي", - "itemHistoryAnalysis": "تحليل تاريخ العنصر", - "pOno": "امر شراء #", + "refundInvoice": "فاتورة الاسترداد", + "hospitalClinic": "المستشفى / العيادة", + "graphicalAnalysis": "التحليل البياني", + "itemHistoryAnalysis": "تحليل سجل الصنف", + "pOno": "رقم أمر الشراء", "oprUnit": "وحدة التشغيل", "qtyOrdered": "الكمية المطلوبة", "qtyReceived": "الكمية المستلمة", - "bonusQty": "كمية المكافأة", - "balQty": "كمية التوازن", + "bonusQty": "الكمية المجانية", + "balQty": "الكمية المتبقية", "videoCall": "مكالمة فيديو", "addUsers": "إضافة مستخدمين إلى المجموعة", "admin": "مشرف", "areYouSureWantTodelete": "هل أنت متأكد أنك تريد الحذف؟", + "confirmGrantMazayaAccess": "هل أنت متأكد أنك تريد منح صلاحية مزايا لـ \"{}\"؟", + "confirmRevokeMazayaAccess": "هل أنت متأكد أنك تريد إلغاء صلاحية مزايا لـ \"{}\"؟", + "yes": "نعم", + "no": "لا", + "mazayaInviteInstructions": "يرجى تحميل تطبيق مهمم من الرابط أعلاه واستخدام بيانات الاعتماد التالية لتسجيل الدخول", + "cameraPermissionRequiredToScan": "يرجى منح صلاحية الكاميرا لمسح رمز الاستجابة السريعة", + "cameraPermissionPermanentlyDenied": "يرجى تفعيل صلاحية الكاميرا من إعدادات التطبيق > الصلاحيات", + "openSettings": "فتح الإعدادات", + "nationalIdOrIqamaFallback": "رقم الهوية الوطنية أو رقم الإقامة الخاص بك", "Attendance": "الحضور", "audioCall": "مكالمة صوتية", "editGroups": "تعديل المجموعة", @@ -618,38 +624,38 @@ "members": "الأعضاء", "searchByUserName": "البحث بواسطة اسم المستخدم", "shareScreen": "مشاركة الشاشة", - "start": "يبدأ", + "start": "بدء", "about": "عن", - "explore": "يستكشف", - "mazaya": "مازيا", - "benefits": "فوائد", + "explore": "استكشاف", + "mazaya": "مزايا", + "benefits": "المزايا", "mazayaDesc": "اكتشف الخصومات والعروض الخاصة المتاحة للموظفين", - "viewallofferMazaya": "أعرض كل المزايا", + "viewallofferMazaya": "عرض جميع عروض مزايا", "buyerName": "اسم المشتري", "buyerNumber": "رقم المشتري", "highestBidder": "أعلى مزايد", "remarks": "ملاحظات", - "faHeader": "تفاصيل رأس الصفحة FA", - "bookTypeCode": "رمز نوع الكتاب", + "faHeader": "بيانات رأس طلب الأصول الثابتة", + "bookTypeCode": "رمز نوع الدفتر", "categoryCode": "رمز الفئة", "categoryGroup": "مجموعة الفئات", "faLINES": "تفاصيل الأصول", - "assetNumber": "رقم الأصول", - "assetDescription": "وصف الأصول", + "assetNumber": "رقم الأصل", + "assetDescription": "وصف الأصل", "barCodeNumber": "رقم الباركود", "datePlaceInService": "تاريخ وضعه في الخدمة", - "serialNumber": "رقم سري", - "disposedDate": "تاريخ التخلص", + "serialNumber": "الرقم التسلسلي", + "disposedDate": "تاريخ الاستبعاد", "netBookValue": "القيمة الدفترية الصافية", "purchasedPrice": "سعر الشراء", "usefulLife": "العمر الإنتاجي", "yearsUsed": "سنوات الاستخدام", - "faRequest": "طلب التخلص من FA", + "faRequest": "طلب استبعاد أصل ثابت", "showMore": "عرض المزيد", "ticketDetails": "تفاصيل التذكرة", "currentTicketBalance": "رصيد التذكرة الحالي", - "ticketHistory": "تاريخ التذكرة", - "proceedBooking": "المتابعة للحجز", + "ticketHistory": "سجل التذاكر", + "proceedBooking": "المتابعة إلى الحجز", "courses": "الدورات التدريبية", "course": "دورة", "status": "الحالة", @@ -659,27 +665,217 @@ "pullToRefresh": "اسحب للتحديث", "courseUrlNotAvailable": "رابط الدورة غير متوفر", "buyerDetails": "تفاصيل المشتري", - "etqanOVR": "الإبلاغ عن حادثة", - "etqanRequest": "طلبات الحوادث", + "etqanOVR": "الإبلاغ عن واقعة", + "etqanRequest": "طلبات الإبلاغ عن الوقائع", "selectProject": "حدد المشروع", "project": "مشروع", - "keen": "حريص", + "keen": "KEEN", "reportIncident": "الإبلاغ عن الحادث", "myIncidents": "حوادثي", "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 أشهر)", + "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": "لا توجد طريقة استخدام محددة.", + "contactInfo": "معلومات التواصل", + "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": "عرض جميع الفروع ({})", + "searchBranches": "البحث عن الفروع أو المدن...", + "allCities": "الكل", + "allCitiesOption": "جميع المدن", + "noBranchesFound": "لم يتم العثور على فروع مطابقة", + "browseAllBranches": "تصفح جميع الفروع ({}) في مختلف المدن", + "branchesCount": "{} فرع", + "kmAway": "على بعد {} كم", + "dependentDetails": "تفاصيل التابع", + "keyDetails": "البيانات الأساسية", + "verified": "تم التحقق", + "idEnding": "رقم الهوية المنتهي بـ {}", + "servicesAndActions": "الخدمات والإجراءات", + "startDependentFlowsDesc": "بدء إجراءات التابع وإدارة الوصول من مكان واحد.", + "mazayaAccess": "الوصول إلى مزايا", + "accessOff": "معطل", + "allowDependentMazayaOffersDesc": "السماح لـ {} باستخدام عروض مزايا بعد التحقق من هويته.", + "loginMethod": "طريقة تسجيل الدخول", + "nationalIdAndDob": "الهوية الوطنية وتاريخ الميلاد", + "sponsorOtp": "رمز التحقق المرسل إلى الموظف المسؤول عن التابع", + "revokingPreventsAccessDesc": "إلغاء الوصول يمنع التابع من استخدام المزايا حتى تعيد السماح به مرة أخرى.", + "locationPermissionRequiredForAttendance": "يجب منح إذن الموقع لتسجيل الحضور", + "locationServicesRequiredForAttendance": "يجب تفعيل خدمة الموقع لتسجيل الحضور", + "pleaseProvideLocationPermission": "الرجاء منح إذن الموقع", + "microphoneAccessRequired": "يحتاج التطبيق إلى إذن الوصول إلى الميكروفون لتسجيل الصوت.", + "shareAccessGuide": "مشاركة دليل الوصول", + "nationalIdOrIqamaNumber": "رقم الهوية الوطنية أو الإقامة", + "multipleCities": "مدن متعددة", + "filters": "الفلاتر", + "availability": "التوفر", + "chooseOneOrBoth": "اختر أحد الخيارين أو كليهما", + "sortBy": "الترتيب حسب", + "recommended": "الموصى بها", + "nearest": "الأقرب", + "highestDiscount": "الأعلى خصمًا", + "newest": "الأحدث", + "distance": "المسافة", + "fromYourCurrentLocation": "من موقعك الحالي", + "any": "الكل", + "minimumDiscount": "أقل نسبة خصم", + "reset": "إعادة تعيين", + "applyFilters": "تطبيق الفلاتر", + "distanceKm": "{} كم", + "discountPercentPlus": "{}%+", + "visitMerchantWebsite": "زيارة موقع التاجر", + "favoriteAddedSuccess": "تمت إضافة التاجر إلى المفضلة بنجاح", + "favoriteRemovedSuccess": "تمت إزالة التاجر من المفضلة بنجاح", + "favoriteError": "تعذّر تحديث المفضلة. يُرجى المحاولة مرة أخرى.", + "merchantWebsiteNotAvailable": "الموقع الإلكتروني للتاجر غير متاح", + "locationServicesRequiredForNearestOffers": "فعّل خدمات الموقع لعرض العروض القريبة منك", + "locationPermissionRequiredForNearestOffers": "اسمح للتطبيق بالوصول إلى موقعك لعرض العروض القريبة منك", + "mazayaDeals": "عروض مزايا", + "helloName": "مرحباً، {}", + "findABrandOrSomething": "ابحث عن علامة تجارية أو شيء بسيط", + "exploreYourBenefits": "استكشف مزاياك", + "theMazayaEdit": "اختيارات مزايا", + "exclusivelyYours":"HMG حصرياً", + "aLittleExtraJustForMazayaMembers": "مزايا إضافية، خاصة بأعضاء مزايا.", + "seeAll": "عرض الكل", + "latestOnMazaya": "أحدث ما في مزايا", + "goodFoodGreatCompanyEvenBetterPerks": "طعام رائع. رفقة ممتعة. مزايا أفضل.", + "travelEssentials": "أساسيات السفر", + "fromEverydayCareToYourNextAdventure": "من العناية اليومية إلى مغامرتك القادمة.", + "homeAndKitchen": "المنزل والمطبخ", + "curatedPicksForYourHomeAndKitchen": "اختيارات مميزة لمنزلك ومطبخك.", + "discover": "اكتشف", + "saved": "المحفوظات", + "myFamily": "عائلتي", + "educationAndTraining": "التعليم والتدريب", + "foodsAndDrinks": "الأطعمة والمشروبات", + "travelAndEntertainment": "السفر والترفيه", + "beautyAndFitness": "الجمال واللياقة", + "healthcare": "الرعاية الصحية", + "favoriteMerchant": "العروض المفضلة", + "favoriteOffers": "العروض المفضلة", + "goodToKnow": "معلومات مهمة", + "goodToKnowDesc": "احتفظ بعضوية مزايا في متناول يدك. تحقق من توفر التاجر والأنشطة المؤهلة والشروط الكاملة قبل الحجز.", + "termsAndConditions": "الشروط والأحكام", + "haveAQuestionAboutOffer": "هل لديك سؤال حول هذا العرض؟", + "getHelp": "الحصول على المساعدة", + "redeemOffer": "استرداد العرض", + "merchantContact": "بيانات التواصل مع التاجر", + "website": "الموقع الإلكتروني", + "phone": "الهاتف", + "viewAll": "عرض الكل", + "moreCitiesCount": "+ {} مدن أخرى", + "mazayaDeals": "عروض مزايا", + "exploreYourBenefits": "استكشف مزاياك", + "findAnEscapeOrAnExperience": "ابحث عن تجربة أو عطلة", + "allOffers": "جميع العروض", + "sampleAvailability": "حسب التوفر", + "offersCount": "{} عروض", + "mazayaExclusive": "حصري لمزايا", + "youAreAllCaughtUp": "لقد شاهدت كل العروض. المزيد قريباً.", + "selectCity": "اختر المدينة", + "searchCities": "البحث عن المدن", + "amuLastThreeMonths": "AMU (آخر 3 أشهر)", "buyerDetails": "تفاصيل المشتري", "chatBot": "المساعد الذكي", "helloAskMe": "مرحباً، اسألني", @@ -691,4 +887,3 @@ "am": "ص", "pm": "م" } - diff --git a/assets/langs/en-US.json b/assets/langs/en-US.json index 6701e29..7292dcb 100644 --- a/assets/langs/en-US.json +++ b/assets/langs/en-US.json @@ -549,6 +549,15 @@ "manage": "Manage", "members": "Members", "areYouSureWantTodelete": "Are you sure want to delete?", + "confirmGrantMazayaAccess": "Are you sure you want to grant Mazaya access to \"{}\"?", + "confirmRevokeMazayaAccess": "Are you sure you want to revoke Mazaya access to \"{}\"?", + "yes": "Yes", + "no": "No", + "mazayaInviteInstructions": "Please download the MoheM app from the above link and use the following credentials to login", + "cameraPermissionRequiredToScan": "Please grant camera permission to scan the QR code", + "cameraPermissionPermanentlyDenied": "Please allow camera permission from the app settings > Permissions", + "openSettings": "Open Settings", + "nationalIdOrIqamaFallback": "Your National ID or Iqama Number", "groupMembers": "Group Members", "manageGroup": "Manage Group", "admin": "Admin", @@ -570,7 +579,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,17 +675,211 @@ "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)", + + + + + "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", + "mazayaAccessRevokedDesc": "You are not authorized to access the Mazaya Program.", + "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.", + "contactInfo": "Contact Information", + "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 - Coming Soon", + "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", + "searchBranches": "Search branches or cities...", + "allCities": "All", + "allCitiesOption": "All Cities", + "noBranchesFound": "No branches found", + "browseAllBranches": "Browse all {} branches across cities", + "branchesCount": "{} Branches", + "kmAway": "{} KM Away", + "dependentDetails": "Dependent Details", + "keyDetails": "Key details", + "verified": "Verified", + "idEnding": "ID ending {}", + "servicesAndActions": "Services & actions", + "startDependentFlowsDesc": "Start dependent flows and manage access from one place.", + "mazayaAccess": "Mazaya access", + "accessOff": "Off", + "allowDependentMazayaOffersDesc": "Allow {} to use Mazaya offers with her verified identity.", + "loginMethod": "Login method", + "nationalIdAndDob": "National ID & Date of Birth", + "sponsorOtp": "Sponsor OTP", + "revokingPreventsAccessDesc": "Revoking prevents all future dependent access until you allow it again.", + "locationPermissionRequiredForAttendance": "You need to give location permission to mark attendance", + "locationServicesRequiredForAttendance": "You need to enable location services to mark attendance", + "pleaseProvideLocationPermission": "Please provide location permission", + "microphoneAccessRequired": "The app needs microphone access to be able to record audio.", + "shareAccessGuide": "Share Access Guide", + "nationalIdOrIqamaNumber": "National ID or Iqama Number", + "multipleCities": "Multi-city", + "filters": "Filters", + "availability": "Availability", + "chooseOneOrBoth": "Choose one or both", + "sortBy": "Sort by", + "recommended": "Recommended", + "nearest": "Nearest", + "highestDiscount": "Highest discount", + "newest": "Newest", + "distance": "Distance", + "fromYourCurrentLocation": "From your current location", + "any": "Any", + "minimumDiscount": "Minimum discount", + "reset": "Reset", + "applyFilters": "Apply filters", + "distanceKm": "{} km", + "discountPercentPlus": "{}%+", + "visitMerchantWebsite": "Visit Merchant Website", + "merchantWebsiteNotAvailable": "Merchant website not available", + "locationServicesRequiredForNearestOffers": "You need to enable location services to show nearest offers", + "locationPermissionRequiredForNearestOffers": "You need to give location permission to show nearest offers", + "mazayaDeals": "Mazaya Deals", + "helloName": "Hello, {}", + "findABrandOrSomething": "Find a brand or a little something", + "exploreYourBenefits": "Explore your benefits", + "theMazayaEdit": "The Mazaya Edit", + "exclusivelyYours": "Exclusively HMG", + "aLittleExtraJustForMazayaMembers": "A little extra, just for Mazaya members.", + "seeAll": "See all", + "latestOnMazaya": "Latest on Mazaya", + "goodFoodGreatCompanyEvenBetterPerks": "Good food. Great company. Even better perks.", + "travelEssentials": "Travel Essentials", + "fromEverydayCareToYourNextAdventure": "From everyday care to your next adventure.", + "homeAndKitchen": "Home & Kitchen", + "curatedPicksForYourHomeAndKitchen": "Curated picks for your home and kitchen.", + "discover": "Discover", + "saved": "Saved", + "myFamily": "My Family", + "educationAndTraining": "Education & Training", + "foodsAndDrinks": "Foods & Drinks", + "travelAndEntertainment": "Travel & Entertainment", + "beautyAndFitness": "Beauty & Fitness", + "healthcare": "Healthcare", + "favoriteAddedSuccess": "Added to favorites successfully", + "favoriteRemovedSuccess": "Removed from favorites successfully", + "favoriteError": "An error occurred while updating favorites", + "favoriteMerchant": "Favorite Offers", + "favoriteOffers": "Favorite Offers", + "goodToKnow": "Good to know", + "goodToKnowDesc": "Keep your Mazaya membership handy. Check the merchant's availability, eligible activities and full terms before you book.", + "termsAndConditions": "Terms & conditions", + "haveAQuestionAboutOffer": "Have a question about this offer?", + "getHelp": "Get help", + "redeemOffer": "Redeem offer", + "merchantContact": "Merchant contact", + "website": "Website", + "phone": "Phone", + "viewAll": "View all", + "moreCitiesCount": "+ {} more cities", + "mazayaDeals": "Mazaya Deals", + "exploreYourBenefits": "Explore your benefits", + "findAnEscapeOrAnExperience": "Find an escape or an experience", + "allOffers": "All offers", + "sampleAvailability": "sample availability", + "offersCount": "{} offers", + "mazayaExclusive": "MAZAYA EXCLUSIVE", + "youAreAllCaughtUp": "You're all caught up. More little extras soon.", + "selectCity": "Select City", + "searchCities": "Search cities", + "amuLastThreeMonths": "AMU (Last 3 months)", "buyerDetails": "Buyer Details", "chatBot": "ChatBot", "helloAskMe": "Hello, ask me", @@ -688,4 +890,5 @@ "initializingChatPleaseWait": "Please wait, initializing chat...", "am": "AM", "pm": "PM" -} \ No newline at end of file +} + diff --git a/assets/pdf/Mazaya Access Guide CS ar.pdf b/assets/pdf/Mazaya Access Guide CS ar.pdf new file mode 100644 index 0000000..6556f4c Binary files /dev/null and b/assets/pdf/Mazaya Access Guide CS ar.pdf differ diff --git a/assets/pdf/Mazaya Access Guide CS en.pdf b/assets/pdf/Mazaya Access Guide CS en.pdf new file mode 100644 index 0000000..b7d767b Binary files /dev/null and b/assets/pdf/Mazaya Access Guide CS en.pdf differ diff --git a/assets/pdf/Mazaya Access Guide HMG ar.pdf b/assets/pdf/Mazaya Access Guide HMG ar.pdf new file mode 100644 index 0000000..d773527 Binary files /dev/null and b/assets/pdf/Mazaya Access Guide HMG ar.pdf differ diff --git a/assets/pdf/Mazaya Access Guide HMG en.pdf b/assets/pdf/Mazaya Access Guide HMG en.pdf new file mode 100644 index 0000000..c7b2911 Binary files /dev/null and b/assets/pdf/Mazaya Access Guide HMG en.pdf differ diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist index 391a902..1dc6cf7 100644 --- a/ios/Flutter/AppFrameworkInfo.plist +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -20,5 +20,7 @@ ???? CFBundleVersion 1.0 + MinimumOSVersion + 13.0 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index fe5b6ee..85b7dec 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -7,8 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + 0AD79F8B1905C17FADC8B42C /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D8F0CC8EAC13AF3E07BEE567 /* Pods_Runner.framework */; }; 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; - 1CCF895CFB3C95DEBB84A05F /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D8F0CC8EAC13AF3E07BEE567 /* Pods_Runner.framework */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 76AE113C28E078EA00513112 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 76AE113B28E078EA00513112 /* GoogleService-Info.plist */; }; @@ -59,8 +59,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, - 1CCF895CFB3C95DEBB84A05F /* Pods_Runner.framework in Frameworks */, + 0AD79F8B1905C17FADC8B42C /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -148,7 +147,7 @@ 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, C4CFBC4C5CAC00182015ACD5 /* [CP] Embed Pods Frameworks */, - D9FFC0F21D4146458615A992 /* [CP] Copy Pods Resources */, + EF29CE25BB57F5AA48CE1136 /* [CP] Copy Pods Resources */, ); buildRules = ( ); diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 5db441f..27924ba 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -3,7 +3,7 @@ LastUpgradeVersion = "1510" version = "1.3"> + + + + sms tel mailto + https + http + comgooglemaps + googlechromes + waze LSRequiresIPhoneOS @@ -67,15 +72,17 @@ NSCameraUsageDescription - This app requires camera access to capture & upload picture as profile image. + This app requires camera access to capture & upload picture as profile image, and scan QR codes. NSFaceIDUsageDescription This app requires Face ID to allow biometric authentication for app login. + NSLocalNetworkUsageDescription + This app requires access to local network to connect to company Wi-Fi networks for attendance. NSLocationAlwaysAndWhenInUseUsageDescription - This App requires access to your location to mark your attendance. + This app requires access to your location to mark your attendance and discover nearby offers and merchants. NSLocationAlwaysUsageDescription - This App requires access to your location to mark your attendance. + This app requires access to your location to mark your attendance and discover nearby offers and merchants. NSLocationWhenInUseUsageDescription - This App requires access to your location to mark your attendance. + This app requires access to your location to mark your attendance and discover nearby offers and merchants. NSMicrophoneUsageDescription This app requires microphone access for calls and speech-to-text input. NSPhotoLibraryUsageDescription diff --git a/ios/Runner/Runner.entitlements b/ios/Runner/Runner.entitlements index 304aa00..98a55e2 100644 --- a/ios/Runner/Runner.entitlements +++ b/ios/Runner/Runner.entitlements @@ -20,6 +20,7 @@ com.apple.developer.nfc.readersession.formats + NDEF TAG com.apple.developer.ubiquity-container-identifiers diff --git a/lib/api/api_client.dart b/lib/api/api_client.dart index 7e346c8..7ac6c8d 100644 --- a/lib/api/api_client.dart +++ b/lib/api/api_client.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:convert'; +import 'dart:developer'; import 'dart:io'; import 'package:flutter/cupertino.dart'; @@ -36,16 +37,24 @@ APIException _throwAPIException(Response response) { case 200: APIError? apiError; if (response.body != null && response.body.isNotEmpty) { - var jsonError = jsonDecode(response.body); - print(jsonError); - apiError = APIError(jsonError['ErrorCode'], jsonError['ErrorMessage'], jsonError['ErrorType'], jsonError['ErrorStatusCode']); + try { + var jsonError = jsonDecode(response.body); + print(jsonError); + apiError = APIError(jsonError['ErrorCode'], jsonError['ErrorMessage'], jsonError['ErrorType'], jsonError['ErrorStatusCode']); + } catch (_) { + log("Non-JSON 200 error body: ${response.body}"); + } } return APIException(APIException.BAD_REQUEST, error: apiError); case 400: APIError? apiError; if (response.body != null && response.body.isNotEmpty) { - var jsonError = jsonDecode(response.body); - apiError = APIError(jsonError['ErrorCode'], jsonError['ErrorMessage'], jsonError['ErrorType'], jsonError['ErrorStatusCode']); + try { + var jsonError = jsonDecode(response.body); + apiError = APIError(jsonError['ErrorCode'], jsonError['ErrorMessage'], jsonError['ErrorType'], jsonError['ErrorStatusCode']); + } catch (_) { + log("Non-JSON 400 error body: ${response.body}"); + } } return APIException(APIException.BAD_REQUEST, error: apiError); case 401: @@ -86,15 +95,14 @@ class ApiClient { _headers.addAll(headers); } if (!kReleaseMode) { - print("Url:$url"); + log("Url: $url"); var bodyJson = json.encode(jsonObject); - print("body:$bodyJson"); + log("body:$bodyJson"); } var response = await postJsonForResponse(url, jsonObject, token: token, queryParameters: queryParameters, headers: _headers, retryTimes: retryTimes, isFormData: isFormData); try { if (!kReleaseMode) { - logger.i("Url: " + url); - logger.i("res: " + response.body); + log("res: " + response.body); } var jsonData = jsonDecode(response.body); @@ -112,7 +120,7 @@ class ApiClient { } else if (jsonData["MessageStatus"] == 2 && jsonData["IsOTPMaxLimitExceed"] == true) { await Utils.performLogout(AppRoutes.navigatorKey.currentContext, null); throw const APIException(APIException.UNAUTHORIZED, error: null); - } else { + } else { APIError? apiError; apiError = APIError(jsonData['ErrorCode'], jsonData['ErrorEndUserMessage'], jsonData['ErrorType'] ?? 0, jsonData['ErrorStatusCode']); throw APIException(APIException.BAD_REQUEST, error: apiError); @@ -121,6 +129,7 @@ class ApiClient { if (ex is APIException) { rethrow; } else { + log("BAD_RESPONSE_FORMAT for $url -> body: ${response.body}"); throw APIException(APIException.BAD_RESPONSE_FORMAT, arguments: ex); } } diff --git a/lib/api/login_api_client.dart b/lib/api/login_api_client.dart index 24d248a..65e5fc6 100644 --- a/lib/api/login_api_client.dart +++ b/lib/api/login_api_client.dart @@ -1,13 +1,20 @@ import 'dart:async'; +import 'dart:io'; +import 'package:firebase_messaging/firebase_messaging.dart'; +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'; +import 'package:sms_otp_auto_verify/sms_otp_auto_verify.dart'; class LoginApiClient { static final LoginApiClient _instance = LoginApiClient._internal(); @@ -16,33 +23,142 @@ class LoginApiClient { factory LoginApiClient() => _instance; - Future getMobileLoginInfoNEW(String deviceToken, String deviceType) async { + static String? _cachedSignature; + + static Future getSignature() async { + if (!Platform.isAndroid) return null; + if (_cachedSignature != null && _cachedSignature!.isNotEmpty) { + return _cachedSignature; + } + try { + String? sig = await SmsVerification.getAppSignature(); + if (sig != null && sig.isNotEmpty) { + _cachedSignature = sig; + } + return sig; + } catch (e) { + debugPrint("Error getting app signature: $e"); + return null; + } + } + + Future getMobileLoginInfoNEW(String deviceToken, String deviceType, {int? memberType}) async { String url = "${ApiConsts.erpRest}Mohemm_GetMobileLoginInfo_NEW"; Map postParams = {}; postParams["DeviceToken"] = deviceToken; postParams["DeviceType"] = deviceType; + postParams["MemberType"] = memberType ?? ApiConsts.selectedMemberType.value; return await ApiClient().postJsonForObject((json) { GenericResponseModel? responseData = GenericResponseModel.fromJson(json); + + if (responseData != null) { + bool? isEmp = responseData.isEmpLastLoginToDevice; + if (isEmp == null && json is Map) { + dynamic raw = json["IsEmpLastLoginToDevice"] ?? json["isEmpLastLoginToDevice"]; + if (raw is bool) { + isEmp = raw; + } else if (raw is num) { + isEmp = raw == 1; + } else if (raw is String) { + isEmp = raw.toLowerCase() == "true" || raw == "1"; + } + } + AppState().setIsEmpLastLoginToDevice = isEmp; + } + return (responseData.mohemmGetMobileLoginInfoList?.length ?? 0) > 0 ? (responseData.mohemmGetMobileLoginInfoList!.first) : null; }, 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) async { + String email, int sessionId, String employeeName, int loginType, String mobileNumber, String userName, String? deviceToken, String deviceType, + {int? memberType}) async { String url = "${ApiConsts.erpRest}Mohemm_InsertMobileLoginInfo"; + String devType = deviceType.isNotEmpty ? deviceType : (Platform.isAndroid ? "android" : "ios"); + String devToken = (deviceToken != null && deviceToken.isNotEmpty && deviceToken != "null") + ? deviceToken + : (AppState().getIsHuawei ? AppState().getHuaweiPushToken : (AppState().getDeviceToken ?? "")); + + if (devToken.isEmpty || devToken == "null") { + try { + String savedToken = await Utils.getStringFromPrefs("P_DEP_DEVICE_TOKEN"); + if (savedToken.isNotEmpty && savedToken != "null") { + devToken = savedToken; + } + } catch (_) {} + } + + if (devToken.isEmpty || devToken == "null") { + try { + if (!AppState().getIsHuawei) { + String? tok = await FirebaseMessaging.instance.getToken(); + if (tok != null && tok.isNotEmpty) { + devToken = tok; + AppState().setDeviceToken = tok; + Utils.saveStringFromPrefs("P_DEP_DEVICE_TOKEN", tok); + } + } + } catch (_) {} + } + Map postParams = { "MobileNumber": mobileNumber, "P_USER_NAME": userName, "UserName": userName, "CompanyID": 1, // todo 'sikander' @discuss umer for companyID - "DeviceToken": deviceToken, + "DeviceToken": devToken, "LoginType": loginType, "EmployeeName": employeeName, "P_SESSION_ID": sessionId, - "P_EMAIL_ADDRESS": email + "P_EMAIL_ADDRESS": email, + "MemberType": memberType ?? ApiConsts.selectedMemberType.value, }; - postParams["DeviceToken"] = deviceToken; - postParams["DeviceType"] = deviceType; + postParams["DeviceToken"] = devToken; + postParams["DeviceType"] = devType; postParams.addAll(AppState().postParamsJson); return await ApiClient().postJsonForObject((json) { GenericResponseModel? responseData = GenericResponseModel.fromJson(json); @@ -51,6 +167,93 @@ 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 != "null") + ? deviceToken + : (AppState().getIsHuawei ? AppState().getHuaweiPushToken : (AppState().getDeviceToken ?? "")); + + if (devToken.isEmpty || devToken == "null") { + try { + String savedToken = await Utils.getStringFromPrefs("P_DEP_DEVICE_TOKEN"); + if (savedToken.isNotEmpty && savedToken != "null") { + devToken = savedToken; + } + } catch (_) {} + } + + if (devToken.isEmpty || devToken == "null") { + try { + if (!AppState().getIsHuawei) { + String? tok = await FirebaseMessaging.instance.getToken(); + if (tok != null && tok.isNotEmpty) { + devToken = tok; + AppState().setDeviceToken = tok; + Utils.saveStringFromPrefs("P_DEP_DEVICE_TOKEN", tok); + } + } + } catch (_) {} + } + + String effectiveToken = (tokenID != null && tokenID.isNotEmpty) + ? tokenID + : (AppState().postParamsObject?.tokenID ?? AppState().authenticationTokenID ?? ""); + + Map postParams = { + "VersionID": versionId ?? 5, + "Channel": channel, + "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 = {}; @@ -69,8 +272,15 @@ class LoginApiClient { }, url, postParams); } - Future mohemmSendActivationCodeByOTPNotificationType(int isMobileFingerPrint, String? mobileNumber, int optSendType, String? pUserName) async { + Future mohemmSendActivationCodeByOTPNotificationType( + int isMobileFingerPrint, + String? mobileNumber, + int optSendType, + String? pUserName, { + String? smsSignature, + }) async { String url = "${ApiConsts.erpRest}Mohemm_SendActivationCodebyOTPNotificationType"; + String? sig = smsSignature ?? await getSignature(); Map postParams = { "IsMobileFingerPrint": isMobileFingerPrint, "MobileNumber": mobileNumber, @@ -80,12 +290,13 @@ class LoginApiClient { "NotificationCenterAppSeviceGUID": ApiConsts.notificationCenterLoginServiceGUID, }; postParams.addAll(AppState().postParamsJson); + postParams["smsSignature"] = sig ?? ""; return await ApiClient().postJsonForObject((json) => GenericResponseModel.fromJson(json).basicMemberInformation, url, postParams); } - Future checkActivationCode(bool isDeviceNFC, String? mobileNumber, String activationCode, String? pUserName) async { + Future checkActivationCode(bool isDeviceNFC, String? mobileNumber, String activationCode, String? pUserName, String loginType) async { String url = "${ApiConsts.erpRest}CheckActivationCode"; - Map postParams = {"IsDeviceNFC": isDeviceNFC, "MobileNumber": mobileNumber, "activationCode": activationCode, "P_USER_NAME": pUserName}; + Map postParams = {"IsDeviceNFC": isDeviceNFC, "MobileNumber": mobileNumber, "activationCode": activationCode, "P_USER_NAME": pUserName, "LoginType":loginType}; postParams.addAll(AppState().postParamsJson); return await ApiClient().postJsonForObject((json) { GenericResponseModel responseData = GenericResponseModel.fromJson(json); @@ -97,8 +308,10 @@ 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; + AppState().setPrivilegeListModel = responseData.privilegeList ?? []; return responseData; }, url, postParams); } @@ -110,8 +323,9 @@ class LoginApiClient { return await ApiClient().postJsonForObject((json) => GenericResponseModel.fromJson(json).basicMemberInformation, url, postParams); } - Future sendPublicActivationCode(String? mobileNumber, String? pUsername) async { + Future sendPublicActivationCode(String? mobileNumber, String? pUsername, {String? smsSignature}) async { String url = "${ApiConsts.erpRest}SendPublicActivationCode"; + String? sig = smsSignature ?? await getSignature(); Map postParams = { "MobileNumber": mobileNumber, "P_MOBILE_NUMBER": mobileNumber, @@ -120,6 +334,8 @@ class LoginApiClient { "NotificationCenterAppSeviceGUID": ApiConsts.notificationCenterForgotPasswordServiceGUID, }; postParams.addAll(AppState().postParamsJson); + postParams["smsSignature"] = sig ?? ""; + postParams["SMSSignature"] = sig ?? ""; return await ApiClient().postJsonForObject((json) { GenericResponseModel responseData = GenericResponseModel.fromJson(json); AppState().postParamsObject?.setLogInTokenID = responseData.logInTokenID; @@ -169,4 +385,144 @@ class LoginApiClient { return responseData; }, url, postParams); } + + 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 = { + "LanguageID": context != null ? AppState().getLanguageID(context) : 2, + "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; + return responseData; + }, url, postParams); + } + + Future mohemmSendOTPDependent({ + required String nationalId, + required String mobileNumber, + required String logInTokenID, + BuildContext? context, + String? smsSignature, + }) async { + String url = "${ApiConsts.erpRest}Mohemm_SendOTP_Dependent"; + String? sig = smsSignature ?? await getSignature(); + Map postParams = { + "VersionID": 20, + "LanguageID": context != null ? AppState().getLanguageID(context) : 2, + "MobileType": Platform.isAndroid ? "android" : "ios", + "OTP_SendType": 1, + "MobileNumber": mobileNumber, + "IsMobileFingerPrint": 0, + "P_DEP_NATIONAL_ID": nationalId, + "P_USER_NAME": nationalId, + "LogInTokenID": logInTokenID, + "NotificationCenterAppGUID": ApiConsts.notificationCenterAppGUID, + "NotificationCenterAppSeviceGUID": ApiConsts.notificationCenterLoginServiceGUID, + "smsSignature": (sig != null && sig.isNotEmpty) ? sig : "", + }; + return await ApiClient().postJsonForObject((json) { + GenericResponseModel responseData = GenericResponseModel.fromJson(json); + return responseData; + }, url, postParams); + } + + Future mohemmCheckOTPDependent({ + required String logInTokenID, + required String activationCode, + required String nationalId, + required String mobileNumber, + BuildContext? context, + }) async { + String url = "${ApiConsts.erpRest}Mohemm_CheckOTP_Dependent"; + Map postParams = { + "LogInTokenID": logInTokenID, + "activationCode": activationCode, + "P_USER_NAME": nationalId, + "P_DEP_NATIONAL_ID": nationalId, + "MobileNumber": mobileNumber, + "LanguageID": context != null ? AppState().getLanguageID(context) : 2, + "MobileType": Platform.isAndroid ? "android" : "ios", + }; + return await ApiClient().postJsonForObject((json) { + GenericResponseModel responseData = GenericResponseModel.fromJson(json); + 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; + AppState().postParamsObject?.setUserName = nationalId; + AppState().postParamsObject?.setPUserName = nationalId; + AppState().postParamsObject?.setMobileNumer = mobileNumber; + AppState().setUserName = nationalId; + 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/marathon/marathon_api_client.dart b/lib/api/marathon/marathon_api_client.dart index d9021c6..2d81c42 100644 --- a/lib/api/marathon/marathon_api_client.dart +++ b/lib/api/marathon/marathon_api_client.dart @@ -104,24 +104,18 @@ class MarathonApiClient { } Future getNextQuestion({required String? questionId, required String marathonId}) async { - Map jsonObject = { - "previousQuestionId": questionId, - "marathonId": marathonId, - }; + Map jsonObject = {"previousQuestionId": questionId, "marathonId": marathonId}; return await ApiClient().postJsonForObject( (json) { MarathonGenericModel marathonModel = MarathonGenericModel.fromJson(json); if (marathonModel.statusCode == 404 || marathonModel.statusCode == 208 || marathonModel.statusCode == 204 || marathonModel.statusCode == 500) { - Utils.confirmDialog( - AppRoutes.navigatorKey.currentContext, + Utils.showToast( marathonModel.message!, - onTap: () { + onComplete: () { AppRoutes.navigatorKey.currentContext!.read().resetValues(); - Navigator.of( - AppRoutes.navigatorKey.currentContext!, - ).popUntil(ModalRoute.withName(AppRoutes.dashboard)); + Navigator.of(AppRoutes.navigatorKey.currentContext!).popUntil(ModalRoute.withName(AppRoutes.dashboard)); }, ); } @@ -214,99 +208,99 @@ class MarathonApiClient { return null; } -// late HubConnection hubConnection; -// Future buildHubConnection(BuildContext context, String prizeId) async { -// HttpConnectionOptions httpOptions = HttpConnectionOptions(skipNegotiation: false, logMessageContent: true); -// hubConnection = HubConnectionBuilder() -// .withUrl( -// ApiConsts.marathonHubConnectionUrl + "?employeeNumber=${AppState().memberInformationList!.eMPLOYEENUMBER ?? ""}&employeeName=${AppState().memberInformationList!.eMPLOYEENAME ?? ""}", -// options: httpOptions, -// ) -// .withAutomaticReconnect( -// retryDelays: [2000, 5000, 10000, 20000], -// ) -// .configureLogging( -// Logger("Logging"), -// ) -// .build(); -// hubConnection.onclose( -// ({Exception? error}) { -// logger.i("onclose"); -// }, -// ); -// hubConnection.onreconnecting( -// ({Exception? error}) { -// logger.i("onreconnecting"); -// }, -// ); -// hubConnection.onreconnected( -// ({String? connectionId}) { -// logger.i("onreconnected"); -// }, -// ); -// if (hubConnection.state != HubConnectionState.Connected) { -// await hubConnection.start(); -// logger.i("Started HubConnection"); -// -// await hubConnection.invoke( -// "AddParticipant", -// args: [ -// { -// "employeeNumber": AppState().memberInformationList!.eMPLOYEENUMBER ?? "", -// "employeeName": AppState().memberInformationList!.eMPLOYEENAME ?? "", -// "marathonId": AppState().getMarathonProjectId, -// "prizeId": "8577B2E8-5DD7-43F0-10DD-08DACB0AC064", -// } -// ], -// ).catchError((e) { -// logger.i("Error in AddParticipant: $e"); -// }); -// -// context.read().addItemToList(ApiConsts.dummyQuestion); -// -// await hubConnection.invoke( -// "SendQuestionToParticipant", -// args: [ -// { -// "marathonId": "${AppState().getMarathonProjectId}", -// } -// ], -// ).catchError((e) { -// Utils.confirmDialog(context, e.toString()); -// logger.i("Error in SendQuestionToParticipant: $e"); -// }); -// -// try { -// hubConnection.on("OnSendQuestionToParticipant", (List? arguments) { -// onSendQuestionToParticipant(arguments, context); -// }); -// } catch (e, s) { -// logger.i("Error in OnSendQuestionToParticipant"); -// } -// -// try { -// hubConnection.on("OnParticipantJoin", (List? arguments) { -// onParticipantJoin(arguments, context); -// }); -// } catch (e, s) { -// logger.i("Error in OnParticipantJoin"); -// } -// } -// } -// -// Future onSendQuestionToParticipant(List? arguments, BuildContext context) async { -// logger.i("onSendQuestionToParticipant arguments: $arguments"); -// -// if (arguments != null) { -// Map data = arguments.first! as Map; -// var json = data["data"]; -// QuestionModel newQuestion = QuestionModel.fromJson(json); -// AppRoutes.navigatorKey.currentContext!.read().onNewQuestionReceived(newQuestion); -// } -// } -// -// Future onParticipantJoin(List? arguments, BuildContext context) async { -// logger.i("OnParticipantJoin arguments: $arguments"); -// context.watch().totalMarathoners++; -// } + // late HubConnection hubConnection; + // Future buildHubConnection(BuildContext context, String prizeId) async { + // HttpConnectionOptions httpOptions = HttpConnectionOptions(skipNegotiation: false, logMessageContent: true); + // hubConnection = HubConnectionBuilder() + // .withUrl( + // ApiConsts.marathonHubConnectionUrl + "?employeeNumber=${AppState().memberInformationList!.eMPLOYEENUMBER ?? ""}&employeeName=${AppState().memberInformationList!.eMPLOYEENAME ?? ""}", + // options: httpOptions, + // ) + // .withAutomaticReconnect( + // retryDelays: [2000, 5000, 10000, 20000], + // ) + // .configureLogging( + // Logger("Logging"), + // ) + // .build(); + // hubConnection.onclose( + // ({Exception? error}) { + // logger.i("onclose"); + // }, + // ); + // hubConnection.onreconnecting( + // ({Exception? error}) { + // logger.i("onreconnecting"); + // }, + // ); + // hubConnection.onreconnected( + // ({String? connectionId}) { + // logger.i("onreconnected"); + // }, + // ); + // if (hubConnection.state != HubConnectionState.Connected) { + // await hubConnection.start(); + // logger.i("Started HubConnection"); + // + // await hubConnection.invoke( + // "AddParticipant", + // args: [ + // { + // "employeeNumber": AppState().memberInformationList!.eMPLOYEENUMBER ?? "", + // "employeeName": AppState().memberInformationList!.eMPLOYEENAME ?? "", + // "marathonId": AppState().getMarathonProjectId, + // "prizeId": "8577B2E8-5DD7-43F0-10DD-08DACB0AC064", + // } + // ], + // ).catchError((e) { + // logger.i("Error in AddParticipant: $e"); + // }); + // + // context.read().addItemToList(ApiConsts.dummyQuestion); + // + // await hubConnection.invoke( + // "SendQuestionToParticipant", + // args: [ + // { + // "marathonId": "${AppState().getMarathonProjectId}", + // } + // ], + // ).catchError((e) { + // Utils.confirmDialog(context, e.toString()); + // logger.i("Error in SendQuestionToParticipant: $e"); + // }); + // + // try { + // hubConnection.on("OnSendQuestionToParticipant", (List? arguments) { + // onSendQuestionToParticipant(arguments, context); + // }); + // } catch (e, s) { + // logger.i("Error in OnSendQuestionToParticipant"); + // } + // + // try { + // hubConnection.on("OnParticipantJoin", (List? arguments) { + // onParticipantJoin(arguments, context); + // }); + // } catch (e, s) { + // logger.i("Error in OnParticipantJoin"); + // } + // } + // } + // + // Future onSendQuestionToParticipant(List? arguments, BuildContext context) async { + // logger.i("onSendQuestionToParticipant arguments: $arguments"); + // + // if (arguments != null) { + // Map data = arguments.first! as Map; + // var json = data["data"]; + // QuestionModel newQuestion = QuestionModel.fromJson(json); + // AppRoutes.navigatorKey.currentContext!.read().onNewQuestionReceived(newQuestion); + // } + // } + // + // Future onParticipantJoin(List? arguments, BuildContext context) async { + // logger.i("OnParticipantJoin arguments: $arguments"); + // context.watch().totalMarathoners++; + // } } 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 8c92539..5c1eaab 100644 --- a/lib/api/offers_and_discounts_api_client.dart +++ b/lib/api/offers_and_discounts_api_client.dart @@ -1,13 +1,24 @@ +import 'dart:async'; import 'dart:convert'; +import 'dart:developer'; +import 'package:flutter/material.dart'; +import 'package:geolocator/geolocator.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/models/generic_response_model.dart'; import 'package:mohem_flutter_app/models/offers_and_discounts/get_categories_list.dart'; import 'package:mohem_flutter_app/models/offers_and_discounts/get_offers_list.dart'; +import 'package:mohem_flutter_app/models/offers_and_discounts/wala_plus_category_model.dart'; +import 'package:mohem_flutter_app/models/offers_and_discounts/wala_plus_city_model.dart'; +import 'package:mohem_flutter_app/models/offers_and_discounts/wala_plus_retailer_model.dart'; +import 'package:mohem_flutter_app/widgets/location/Location.dart' as location; class OffersAndDiscountsApiClient { - static final OffersAndDiscountsApiClient _instance = OffersAndDiscountsApiClient._internal(); + static final OffersAndDiscountsApiClient _instance = + OffersAndDiscountsApiClient._internal(); + static List? cachedCategories; OffersAndDiscountsApiClient._internal(); @@ -17,7 +28,12 @@ class OffersAndDiscountsApiClient { List getSaleCategoriesList = []; String url = "${ApiConsts.cocRest}Mohemm_ITG_GetCategories"; - Map postParams = {"EmployeeNumber": AppState().memberInformationList?.eMPLOYEENUMBER, "ItgPageSize": 100, "ItgPageNo": 1, "ItgIsActive": true}; + Map postParams = { + "EmployeeNumber": AppState().memberInformationList?.eMPLOYEENUMBER, + "ItgPageSize": 100, + "ItgPageNo": 1, + "ItgIsActive": true, + }; postParams.addAll(AppState().postParamsJson); return await ApiClient().postJsonForObject( @@ -44,11 +60,19 @@ class OffersAndDiscountsApiClient { ); } - Future> getOffersList(int categoryID, int pageSize) async { + Future> getOffersList( + int categoryID, + int pageSize, + ) async { List getSaleCategoriesList = []; String url = "${ApiConsts.cocRest}GetOfferDiscountsConfigData"; - Map postParams = {"EmployeeNumber": AppState().memberInformationList?.eMPLOYEENUMBER, "ItgPageSize": pageSize, "ItgPageNo": 1, "ItgCategoryID": categoryID}; + Map postParams = { + "EmployeeNumber": AppState().memberInformationList?.eMPLOYEENUMBER, + "ItgPageSize": pageSize, + "ItgPageNo": 1, + "ItgCategoryID": categoryID, + }; postParams.addAll(AppState().postParamsJson); return await ApiClient().postJsonForObject( @@ -68,4 +92,918 @@ class OffersAndDiscountsApiClient { postParams, ); } + + String _resolveUserName() { + String? pUserName = AppState().getUserName; + if (pUserName == null || pUserName.isEmpty) { + pUserName = + AppState().postParamsObject?.userName ?? + AppState().postParamsObject?.pUserName ?? + AppState().memberInformationList?.eMPLOYEENUMBER; + } + return pUserName ?? ""; + } + + // Same Location.isEnabled -> Location.havePermission -> Location.getCurrentLocation pattern + // used by mark_attendance_widget.dart (e.g. Wifi attendance), wrapped as a Future since this + // is a background enrichment of the request, not a blocking user action - any failure (service + // disabled, permission denied, error) just resolves to null so the retailer fetch still proceeds + // without WP_Geo_Location rather than showing permission dialogs here. + Future _getCurrentPositionSilently(BuildContext context) async { + Completer enabledCompleter = Completer(); + location.Location.isEnabled((bool isEnabled) { + if (!enabledCompleter.isCompleted) enabledCompleter.complete(isEnabled); + }); + bool isEnabled = await enabledCompleter.future; + debugPrint( + "_getCurrentPositionSilently: location service isEnabled=$isEnabled", + ); + if (!isEnabled) return null; + + Completer permissionCompleter = Completer(); + location.Location.havePermission((bool hasPermission) { + if (!permissionCompleter.isCompleted) + permissionCompleter.complete(hasPermission); + }); + bool hasPermission = await permissionCompleter.future; + debugPrint("_getCurrentPositionSilently: havePermission=$hasPermission"); + if (!hasPermission) return null; + + Completer positionCompleter = Completer(); + location.Location.getCurrentLocation( + (Position position, bool isMocked) { + debugPrint( + "_getCurrentPositionSilently: got position lat=${position.latitude}, lng=${position.longitude}, isMocked=$isMocked", + ); + if (!positionCompleter.isCompleted) + positionCompleter.complete(position); + }, + () { + debugPrint( + "_getCurrentPositionSilently: getCurrentLocation error callback fired", + ); + if (!positionCompleter.isCompleted) positionCompleter.complete(null); + }, + context, + ); + return positionCompleter.future; + } + + String _resolveDepNationalId() { + if (!ApiConsts.isDependetLogin) { + return ""; + } + return AppState().getUserName ?? ""; + } + + String _resolveMobileNumber({String? overrideMobile}) { + if (overrideMobile != null && overrideMobile.trim().isNotEmpty) { + return overrideMobile.trim(); + } + + // postParamsObject.mobileNumber/memberInformationList/memberLoginList are only ever set by + // the employee login flow and never cleared on logout - for a dependent session, the only + // legitimate source is the sponsor's mobile captured at dependent login (walaplusDepInfoModel), + // same reasoning as _resolveUserReference above. + if (ApiConsts.isDependetLogin) { + String? depMobile = AppState().walaplusDepInfoModel?.pEmpMobileNumber; + if (depMobile != null && depMobile.trim().isNotEmpty) { + return depMobile.trim(); + } + return ""; + } + + 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 ""; + } + + 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; + } + + 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); + } + + // Pass parentId to fetch the subcategories under a given main category (WP_ParentId) - + // omit it (the default) to fetch the top-level main categories, same as before. + Future> getWalaPlusCategories({ + BuildContext? context, + String? mobileNumber, + String? parentId, + }) 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; + if (parentId != null && parentId.trim().isNotEmpty) { + postParams["WP_Parent_Id"] = parentId.trim(); + } + _cleanWalaPlusParams(postParams); + + List list = await ApiClient().postJsonForObject( + (response) { + List categories = []; + if (response != null) { + dynamic cats = + response['WalaPlus_Categories'] ?? + response['walaPlus_Categories'] ?? + response['WalaPlusCategories'] ?? + response['result']; + if (cats is String) { + try { + cats = json.decode(cats); + } catch (e) { + debugPrint("Error decoding WalaPlus_Categories: $e"); + } + } + if (cats is List) { + cats.forEach((v) { + if (v is Map) { + categories.add(WalaPlusCategoryModel.fromJson(v)); + } else if (v is Map) { + categories.add( + WalaPlusCategoryModel.fromJson(Map.from(v)), + ); + } + }); + } + } + return categories; + }, + url, + postParams, + ); + if ((parentId == null || parentId.trim().isEmpty) && list.isNotEmpty) { + cachedCategories = list; + } + return list; + } + + // Fetched fresh every time the city picker opens (no caching) - see + // mazaya_city_selector.dart. Only P_USER_NAME/MobileNumber (resolved the same way as every + // other WalaPlus call) and WP_Country vary per call; everything else is the shared default + // WalaPlus params/token from _getBaseWalaPlusParams. + Future> getWalaPlusCities({ + String? mobileNumber, + String? country, + }) async { + String url = ApiConsts.walaPlusCities; + String pUserName = _resolveUserName(); + String pMobileNumber = _resolveMobileNumber(overrideMobile: mobileNumber); + + Map postParams = _getBaseWalaPlusParams(); + postParams["P_USER_NAME"] = pUserName; + if (pMobileNumber.isNotEmpty) { + postParams["MobileNumber"] = pMobileNumber; + } + postParams["WP_Country"] = country ?? ""; + _cleanWalaPlusParams(postParams); + + return await ApiClient().postJsonForObject( + (response) { + List cities = []; + if (response != null) { + dynamic data = + response['WalaPlus_Cities'] ?? response['walaPlus_Cities']; + if (data is String) { + try { + data = json.decode(data); + } catch (e) { + debugPrint("Error decoding WalaPlus_Cities: $e"); + } + } + if (data is List) { + for (var v in data) { + if (v is Map) { + cities.add(WalaPlusCityModel.fromJson(v)); + } else if (v is Map) { + cities.add( + WalaPlusCityModel.fromJson(Map.from(v)), + ); + } + } + } + } + return cities; + }, + url, + postParams, + ); + } + + // Confirmed with backend team: WP_Offer_Type accepts "online" and "store". + static const String walaPlusOfferTypeOnline = "online"; + static const String walaPlusOfferTypeInStore = "store"; + + // Maps the screen's internal filter key ("online"/"instore"/"all") to the WP_Offer_Type value + // the backend expects. Returns null for "all" (or anything unrecognized) so callers can skip + // sending WP_Offer_Type entirely. + static String? mapOfferTypeFilter(String internalKey) { + switch (internalKey) { + case "online": + return walaPlusOfferTypeOnline; + case "instore": + return walaPlusOfferTypeInStore; + default: + return null; + } + } + + Future getWalaPlusRetailers({ + String? categoryId, + // Every existing caller passes categoryId (even "" for "all") and relies on WP_CategoryID + // always being present in the request - default true preserves that for everyone. Pass + // false only when the category filter must be fully omitted (e.g. a cross-category newest + // feed), not just set to empty. + bool includeCategoryId = true, + int page = 1, + int perPage = 10, + String? geoLocation, + String? offerType, + String? sortBy, + int? radius, + int? minDiscount, + // Confirmed with backend team: WP_City_ID=3 or WP_City_ID=0 both mean "all cities" (no + // filter) - any other id filters to that specific city. Default null -> 0, same "always + // send a concrete int" contract as WP_Radius/WP_Min_Discount below. + int? cityId, + BuildContext? context, + String? mobileNumber, + String? query, + String? userReference, + }) async { + String url = ApiConsts.walaPlusRetailers; + 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; + } + postParams["MemberType"] = ApiConsts.isDependetLogin ? 2 : 1; + postParams["CompanyID"] = 1; + if (includeCategoryId) { + postParams["WP_CategoryID"] = effectiveCatId; + } + if (offerType != null && offerType.trim().isNotEmpty) { + postParams["WP_Offer_Type"] = offerType.trim(); + } + postParams["WP_Page"] = page; + postParams["WP_Per_page"] = perPage; + if (query != null && query.trim().isNotEmpty) { + postParams["WP_Query"] = query.trim(); + } + if (sortBy != null && sortBy.trim().isNotEmpty) { + postParams["WP_Sort_By"] = sortBy.trim(); + } + // Backend contract declares these as non-nullable int DataMembers, so "Any"/"All" (null + // here) must be sent as a concrete 0, not omitted. + postParams["WP_Radius"] = radius ?? 0; + postParams["WP_Min_Discount"] = minDiscount ?? 0; + postParams["WP_City_ID"] = cityId ?? 0; + + if (geoLocation != null && geoLocation.isNotEmpty) { + postParams["WP_Geo_Location"] = geoLocation; + } else if (context != null) { + try { + Position? position = await _getCurrentPositionSilently(context); + if (position != null) { + postParams["WP_Geo_Location"] = + "${position.latitude},${position.longitude}"; + } + } catch (e) { + debugPrint("getWalaPlusRetailers: unable to get current location: $e"); + } + } + _cleanWalaPlusParams(postParams); + + // Pass WP_User_Reference: if filled then user ID, else null value + if (userReference != null && userReference.trim().isNotEmpty) { + postParams["WP_User_Reference"] = userReference.trim(); + } else { + postParams["WP_User_Reference"] = null; + } + + return await ApiClient().postJsonForObject( + (response) { + if (response != null) { + dynamic val = + response['WalaPlus_Retailers'] ?? + response['walaPlus_Retailers'] ?? + response['WalaPlusRetailers'] ?? + response['WP_Retailers'] ?? + response['result']; + if (val is String) { + 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; + }, + url, + postParams, + ); + } + + Future getWalaPlusRetailerById({ + required String retailerId, + BuildContext? context, + String? mobileNumber, + String? userReference, + }) async { + String url = ApiConsts.walaPlusRetailersByID; + String pUserName = _resolveUserName(); + String pMobileNumber = _resolveMobileNumber(overrideMobile: mobileNumber); + String pDepNationalId = _resolveDepNationalId(); + String pUserReference = _resolveUserReference( + overrideUserReference: userReference, + ); + + 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; + + if (context != null) { + postParams["LanguageID"] = AppState().getLanguageID(context); + try { + Position? position = await _getCurrentPositionSilently(context); + log("context position: $position"); + + if (position != null) { + postParams["WP_Geo_Location"] = + "${position.latitude},${position.longitude}"; + } + } catch (e) { + debugPrint( + "getWalaPlusRetailerById: unable to get current location: $e", + ); + } + } + + _cleanWalaPlusParams(postParams); + + // Pass WP_User_Reference: if filled then user ID, else null value + if (pUserReference.isNotEmpty) { + postParams["WP_User_Reference"] = pUserReference; + } else { + postParams["WP_User_Reference"] = null; + } + + debugPrint( + "getWalaPlusRetailerById: calling $url with params: $postParams", + ); + + return await ApiClient().postJsonForObject( + (response) { + 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) { + 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) { + model = WalaPlusRetailerModel.fromJson(item); + } else if (item is Map) { + 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?.isFavorite == null) { + if (map.containsKey('isfavorite')) { + model?.isFavorite = + map['isfavorite'] == true || + map['isfavorite'] == 1 || + map['isfavorite'] == 'true' || + map['isfavorite'] == '1'; + } else if (map.containsKey('is_favorite')) { + model?.isFavorite = + map['is_favorite'] == true || + map['is_favorite'] == 1 || + map['is_favorite'] == 'true' || + map['is_favorite'] == '1'; + } + } + 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'], + ); + } + if ((model?.website == null || + model!.website!.isEmpty || + model!.website == "null")) { + String? web = + (map['website'] ?? + map['merchant_website'] ?? + map['website_url'] ?? + map['url'] ?? + map['merchant_url']) + ?.toString(); + if (web != null && web.trim().isNotEmpty && web != "null") { + model?.website = web.trim(); + } + } + if ((model?.categories == null || model!.categories!.isEmpty) && + (map.containsKey('categories') || + map.containsKey('Categories') || + map.containsKey('category') || + map.containsKey('Category'))) { + dynamic catData = + map['categories'] ?? + map['Categories'] ?? + map['category'] ?? + map['Category']; + if (catData is List) { + model?.categories = + catData + .map( + (c) => + c is Map + ? WalaPlusCategoryModel.fromJson( + Map.from(c), + ) + : WalaPlusCategoryModel(name: c.toString()), + ) + .toList(); + } else if (catData is Map) { + model?.categories = [ + WalaPlusCategoryModel.fromJson( + Map.from(catData), + ), + ]; + } else if (catData is String && catData.trim().isNotEmpty) { + model?.categories = [ + WalaPlusCategoryModel(name: catData.trim()), + ]; + } + } + } else if (response is Map && + response.containsKey('name')) { + model = WalaPlusRetailerModel.fromJson(response); + } + + if (model != null) { + if (response is Map) { + if (model.isFavorite == null) { + if (response.containsKey('isfavorite')) { + model.isFavorite = + response['isfavorite'] == true || + response['isfavorite'] == 1 || + response['isfavorite'] == 'true' || + response['isfavorite'] == '1'; + } else if (response.containsKey('is_favorite')) { + model.isFavorite = + response['is_favorite'] == true || + response['is_favorite'] == 1 || + response['is_favorite'] == 'true' || + response['is_favorite'] == '1'; + } + } + if (model.website == null || + model.website!.isEmpty || + model.website == "null") { + String? web = + (response['website'] ?? + response['merchant_website'] ?? + response['website_url'] ?? + response['url'] ?? + response['merchant_url']) + ?.toString(); + if (web != null && web.trim().isNotEmpty && web != "null") { + model.website = web.trim(); + } + } + 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'], + ); + } + if ((model.categories == null || model.categories!.isEmpty) && + (response.containsKey('categories') || + response.containsKey('Categories') || + response.containsKey('category') || + response.containsKey('Category'))) { + dynamic catData = + response['categories'] ?? + response['Categories'] ?? + response['category'] ?? + response['Category']; + if (catData is List) { + model.categories = + catData + .map( + (c) => + c is Map + ? WalaPlusCategoryModel.fromJson( + Map.from(c), + ) + : WalaPlusCategoryModel(name: c.toString()), + ) + .toList(); + } else if (catData is Map) { + model.categories = [ + WalaPlusCategoryModel.fromJson( + Map.from(catData), + ), + ]; + } else if (catData is String && catData.trim().isNotEmpty) { + model.categories = [ + WalaPlusCategoryModel(name: catData.trim()), + ]; + } + } + } + debugPrint( + "getWalaPlusRetailerById: parsed retailer '${model.name}', isFavorite: ${model.isFavorite}, offers: ${model.offers?.length ?? 0}, locations: ${model.locations?.length ?? 0}", + ); + return model; + } + } + return null; + }, + url, + postParams, + ); + } + + String resolveUserReference({String? overrideUserReference}) { + return _resolveUserReference(overrideUserReference: overrideUserReference); + } + + String _resolveUserReference({String? overrideUserReference}) { + if (overrideUserReference != null && + overrideUserReference.trim().isNotEmpty) { + return overrideUserReference.trim(); + } + + // AppState().memberInformationList is never cleared on logout, so after logging out of an + // employee session and logging back in as a dependent (without a full app restart) it can + // still be sitting there from the previous session. Checking the CURRENT session type first + // - rather than just whichever field happens to be non-empty - prevents a dependent session + // from ever resolving to the previous employee's number. + if (ApiConsts.isDependetLogin) { + String? depRef = AppState().getUserName; + if (depRef != null && depRef.trim().isNotEmpty) { + return depRef.trim(); + } + return ""; + } + + String? ref = AppState().memberInformationList?.eMPLOYEENUMBER?.toString(); + if (ref != null && ref.trim().isNotEmpty) { + return ref.trim(); + } + String? pUserName = AppState().getUserName; + if (pUserName != null && pUserName.trim().isNotEmpty) { + return pUserName.trim(); + } + return ""; + } + + Future?> redeemWalaPlusOffer({ + required String offerId, + String? offerType, + String? userReference, + BuildContext? context, + }) async { + String url = ApiConsts.walaPlusOfferRedeem; + String pUserName = _resolveUserName(); + String pUserReference = _resolveUserReference( + overrideUserReference: userReference, + ); + + Map postParams = _getBaseWalaPlusParams(); + postParams["WP_User_Reference"] = pUserReference; + postParams["WP_Offer_Id"] = offerId; + postParams["WP_Offer_Type"] = + (offerType != null && offerType.trim().isNotEmpty) + ? offerType.trim() + : "promo"; + postParams["UserName"] = pUserName; + postParams["P_USER_NAME"] = pUserName; + + if (ApiConsts.isDependetLogin) { + String pDepNationalId = _resolveDepNationalId(); + if (pDepNationalId.isNotEmpty) { + postParams["P_DEP_NATIONAL_ID"] = pDepNationalId; + } + postParams["MemberType"] = 2; + } else { + postParams["P_DEP_NATIONAL_ID"] = ""; + postParams["MemberType"] = 1; + } + _cleanWalaPlusParams(postParams); + + debugPrint("redeemWalaPlusOffer calling $url with params: $postParams"); + + return await ApiClient().postJsonForObject( + (response) { + if (response != null) { + debugPrint("redeemWalaPlusOffer response: $response"); + dynamic val = + response['WalaPlus_OfferRedeem'] ?? + response['walaPlus_OfferRedeem'] ?? + response['WalaPlusOfferRedeem'] ?? + response['result']; + if (val is String) { + try { + val = json.decode(val); + } catch (e) { + debugPrint("Error decoding WalaPlus_OfferRedeem: $e"); + } + } + if (val is Map) { + return val; + } else if (val is Map) { + return Map.from(val); + } + } + return null; + }, + url, + postParams, + ); + } + + Future addWalaPlusFavorite({ + Object? retailerId, + List? retailerIds, + BuildContext? context, + String? mobileNumber, + String? userReference, + }) async { + String url = ApiConsts.walaPlusFavoritesAdd; + String pUserName = _resolveUserName(); + String pMobileNumber = _resolveMobileNumber(overrideMobile: mobileNumber); + String pUserReference = _resolveUserReference( + overrideUserReference: userReference, + ); + + List ids = []; + if (retailerIds != null && retailerIds.isNotEmpty) { + ids.addAll(retailerIds); + } else if (retailerId != null) { + if (retailerId is List) { + ids.addAll(retailerId.map((e) => e.toString())); + } else { + ids.add(retailerId.toString()); + } + } + + Map postParams = _getBaseWalaPlusParams(); + postParams["P_USER_NAME"] = pUserName; + postParams["UserName"] = pUserName; + if (pMobileNumber.isNotEmpty) { + postParams["MobileNumber"] = pMobileNumber; + } + if (pUserReference.isNotEmpty) { + postParams["WP_User_Reference"] = pUserReference; + } + postParams["WP_Retailer_Ids"] = ids; + + if (context != null) { + postParams["LanguageID"] = AppState().getLanguageID(context); + } + + if (ApiConsts.isDependetLogin) { + String pDepNationalId = _resolveDepNationalId(); + if (pDepNationalId.isNotEmpty) { + postParams["P_DEP_NATIONAL_ID"] = pDepNationalId; + } + postParams["MemberType"] = 2; + } else { + postParams["MemberType"] = 1; + } + + _cleanWalaPlusParams(postParams); + + debugPrint("addWalaPlusFavorite calling $url with params: $postParams"); + + return await ApiClient().postJsonForObject( + (response) { + if (response != null) { + debugPrint("addWalaPlusFavorite response: $response"); + return GenericResponseModel.fromJson(response); + } + return null; + }, + url, + postParams, + ); + } + + Future removeWalaPlusFavorite({ + Object? retailerId, + List? retailerIds, + BuildContext? context, + String? mobileNumber, + String? userReference, + }) async { + String url = ApiConsts.walaPlusFavoritesRemove; + String pUserName = _resolveUserName(); + String pMobileNumber = _resolveMobileNumber(overrideMobile: mobileNumber); + String pUserReference = _resolveUserReference( + overrideUserReference: userReference, + ); + + List ids = []; + if (retailerIds != null && retailerIds.isNotEmpty) { + ids.addAll(retailerIds); + } else if (retailerId != null) { + if (retailerId is List) { + ids.addAll(retailerId.map((e) => e.toString())); + } else { + ids.add(retailerId.toString()); + } + } + + Map postParams = _getBaseWalaPlusParams(); + postParams["P_USER_NAME"] = pUserName; + postParams["UserName"] = pUserName; + if (pMobileNumber.isNotEmpty) { + postParams["MobileNumber"] = pMobileNumber; + } + if (pUserReference.isNotEmpty) { + postParams["WP_User_Reference"] = pUserReference; + } + postParams["WP_Retailer_Ids"] = ids; + + if (context != null) { + postParams["LanguageID"] = AppState().getLanguageID(context); + } + + if (ApiConsts.isDependetLogin) { + String pDepNationalId = _resolveDepNationalId(); + if (pDepNationalId.isNotEmpty) { + postParams["P_DEP_NATIONAL_ID"] = pDepNationalId; + } + postParams["MemberType"] = 2; + } else { + postParams["MemberType"] = 1; + } + + _cleanWalaPlusParams(postParams); + + debugPrint("removeWalaPlusFavorite calling $url with params: $postParams"); + + return await ApiClient().postJsonForObject( + (response) { + if (response != null) { + debugPrint("removeWalaPlusFavorite response: $response"); + return GenericResponseModel.fromJson(response); + } + return null; + }, + url, + postParams, + ); + } } diff --git a/lib/api/profile_api_client.dart b/lib/api/profile_api_client.dart index 342ac31..7d58478 100644 --- a/lib/api/profile_api_client.dart +++ b/lib/api/profile_api_client.dart @@ -1,9 +1,11 @@ import 'dart:async'; import 'dart:convert'; +import 'dart:io'; 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/date_uitl.dart'; import 'package:mohem_flutter_app/models/generic_response_model.dart'; import 'package:mohem_flutter_app/models/get_approves_list_model.dart'; import 'package:mohem_flutter_app/models/get_contact_dff_structure_list.dart'; @@ -24,8 +26,29 @@ import 'package:mohem_flutter_app/models/profile/submit_address_transaction.dart import 'package:mohem_flutter_app/models/profile/submit_basic_details_transaction_model.dart'; import 'package:mohem_flutter_app/models/profile/submit_contact_transaction_list_model.dart'; import 'package:mohem_flutter_app/models/profile/submit_phone_transactions.dart'; +import 'package:mohem_flutter_app/models/profile/patient_info_model.dart'; +import 'package:mohem_flutter_app/classes/utils.dart'; import 'package:mohem_flutter_app/models/start_phone_approval_process_model.dart'; +// The ERP WCF service's DataContractJsonSerializer rejects any DateTime-typed field unless +// it's wrapped as "/Date(ms)/" - confirmed by its own error on this exact call: +// "DateTime content '6/23/1989 12:00:00 AM' does not start with '/Date(' and end with ')/' +// as required for JSON." GET_EMPLOYEE_CONTACTS hands DATE_OF_BIRTH back as a plain +// "M/d/yyyy h:mm:ss a" string, so it has to be converted before going back out as P_BIRTH_DATE. +String? _toWcfJsonDate(rawDate) { + if (rawDate == null) return null; + String raw = rawDate.toString().trim(); + if (raw.isEmpty) return null; + if (raw.contains('/Date(')) return raw; + try { + DateTime parsed = DateUtil.convertSimpleStringDateToDate(raw); + int utcMillis = DateTime.utc(parsed.year, parsed.month, parsed.day).millisecondsSinceEpoch; + return "/Date($utcMillis)/"; + } catch (_) { + return null; + } +} + class ProfileApiClient { static final ProfileApiClient _instance = ProfileApiClient._internal(); @@ -33,99 +56,140 @@ class ProfileApiClient { factory ProfileApiClient() => _instance; - Future> getEmployeeContacts() async { - String url = "${ApiConsts.erpRest}GET_EMPLOYEE_CONTACTS"; + // GET_EMP_BUSINESS_CARD_INFO requires the literal TokenID below to validate the call - + // it is not the usual session TokenID, so we don't merge AppState().postParamsJson here. + Future getEmployeeBusinessCardInfo() async { + String url = "${ApiConsts.erpRest}GET_EMP_BUSINESS_CARD_INFO"; + String deviceToken = AppState().getIsHuawei ? AppState().getHuaweiPushToken : (AppState().getDeviceToken ?? ""); Map postParams = { - "P_MENU_TYPE": "E", - "P_SELECTED_RESP_ID": -999, + "VersionID": AppState().postParamsObject?.versionID, + "Channel": AppState().postParamsObject?.channel, + "LanguageID": AppState().postParamsObject?.languageID, + "TokenID": "GET_EMP_BUSINESS_CARD_INFO", + "P_USER_NAME": AppState().getUserName ?? AppState().postParamsObject?.userName ?? AppState().postParamsObject?.pUserName, + "DeviceToken": deviceToken, + "DeviceType": Platform.isAndroid ? "android" : "ios", }; + print(jsonEncode(postParams)); + return await ApiClient().postJsonForObject( + (json) { + return GenericResponseModel.fromJson(json); + }, + url, + postParams, + ); + } + + Future> getEmployeeContacts() async { + String url = "${ApiConsts.erpRest}GET_EMPLOYEE_CONTACTS"; + Map postParams = {"P_MENU_TYPE": "E", "P_SELECTED_RESP_ID": -999}; postParams.addAll(AppState().postParamsJson); - return await ApiClient().postJsonForObject((json) { - GenericResponseModel? responseData = GenericResponseModel.fromJson(json); - return responseData.getEmployeeContactsList ?? []; - }, url, postParams); + return await ApiClient().postJsonForObject( + (json) { + GenericResponseModel? responseData = GenericResponseModel.fromJson(json); + return responseData.getEmployeeContactsList ?? []; + }, + url, + postParams, + ); } Future> getEmployeeBasicDetails() async { String url = "${ApiConsts.erpRest}GET_EMPLOYEE_BASIC_DETAILS"; - Map postParams = { - "P_MENU_TYPE": "E", - "P_SELECTED_RESP_ID": -999, - }; + Map postParams = {"P_MENU_TYPE": "E", "P_SELECTED_RESP_ID": -999}; postParams.addAll(AppState().postParamsJson); - return await ApiClient().postJsonForObject((json) { - GenericResponseModel? responseData = GenericResponseModel.fromJson(json); - return responseData.getEmployeeBasicDetailsList ?? []; - }, url, postParams); + return await ApiClient().postJsonForObject( + (json) { + GenericResponseModel? responseData = GenericResponseModel.fromJson(json); + return responseData.getEmployeeBasicDetailsList ?? []; + }, + url, + postParams, + ); } Future> getEmployeePhones() async { String url = "${ApiConsts.erpRest}GET_EMPLOYEE_PHONES"; - Map postParams = { - "P_MENU_TYPE": "E", - "P_SELECTED_RESP_ID": -999, - }; + Map postParams = {"P_MENU_TYPE": "E", "P_SELECTED_RESP_ID": -999}; postParams.addAll(AppState().postParamsJson); - return await ApiClient().postJsonForObject((json) { - GenericResponseModel? responseData = GenericResponseModel.fromJson(json); - return responseData.getEmployeePhonesList ?? []; - }, url, postParams); + return await ApiClient().postJsonForObject( + (json) { + GenericResponseModel? responseData = GenericResponseModel.fromJson(json); + return responseData.getEmployeePhonesList ?? []; + }, + url, + postParams, + ); } Future> getEmployeeAddress() async { String url = "${ApiConsts.erpRest}GET_EMPLOYEE_ADDRESS"; - Map postParams = { - "P_MENU_TYPE": "E", - "P_SELECTED_RESP_ID": -999, - }; + Map postParams = {"P_MENU_TYPE": "E", "P_SELECTED_RESP_ID": -999}; postParams.addAll(AppState().postParamsJson); - return await ApiClient().postJsonForObject((json) { - GenericResponseModel? responseData = GenericResponseModel.fromJson(json); - return responseData.getEmployeeAddressList ?? []; - }, url, postParams); + return await ApiClient().postJsonForObject( + (json) { + GenericResponseModel? responseData = GenericResponseModel.fromJson(json); + return responseData.getEmployeeAddressList ?? []; + }, + url, + postParams, + ); } Future> getEmployeeDocuments() async { String url = "${ApiConsts.erpRest}GET_EMPLOYEE_DOCUMENTS"; - Map postParams = { - "P_MENU_TYPE": "E", - "P_SELECTED_RESP_ID": -999, - }; + Map postParams = {"P_MENU_TYPE": "E", "P_SELECTED_RESP_ID": -999}; postParams.addAll(AppState().postParamsJson); - return await ApiClient().postJsonForObject((json) { - GenericResponseModel? responseData = GenericResponseModel.fromJson(json); - return responseData.employeeDocumentsList ?? []; - }, url, postParams); + return await ApiClient().postJsonForObject( + (json) { + GenericResponseModel? responseData = GenericResponseModel.fromJson(json); + return responseData.employeeDocumentsList ?? []; + }, + url, + postParams, + ); } Future updateEmpImage(img) async { String url = "${ApiConsts.erpRest}UPDATE_EMPLOYEE_IMAGE"; Map postParams = {"P_MENU_TYPE": "E", "P_SELECTED_RESP_ID": -999, "P_IMAGE": img}; postParams.addAll(AppState().postParamsJson); - return await ApiClient().postJsonForObject((json) { - // GenericResponseModel? responseData = GenericResponseModel.fromJson(json); - return json['UpdateEmployeeImageList']; - }, url, postParams); + return await ApiClient().postJsonForObject( + (json) { + // GenericResponseModel? responseData = GenericResponseModel.fromJson(json); + return json['UpdateEmployeeImageList']; + }, + url, + postParams, + ); } Future getDffStructure(String pFunctionName, String uRL, String requestType) async { String url = ApiConsts.erpRest + uRL; Map postParams = {"P_SELECTED_RESP_ID": -999, "P_MENU_TYPE": "E", "P_REQUEST_TYPE": pFunctionName, "P_FUNCTION_NAME": pFunctionName, "P_COUNTRY_CODE": "SA"}; postParams.addAll(AppState().postParamsJson); - return await ApiClient().postJsonForObject((json) { - GenericResponseModel? responseData = GenericResponseModel.fromJson(json); - return responseData; - }, url, postParams); + return await ApiClient().postJsonForObject( + (json) { + GenericResponseModel? responseData = GenericResponseModel.fromJson(json); + return responseData; + }, + url, + postParams, + ); } Future getColStructure(String pFunctionName, String uRL, String requestType) async { String url = ApiConsts.erpRest + uRL; Map postParams = {"P_SELECTED_RESP_ID": -999, "P_MENU_TYPE": "E", "P_FUNCTION_NAME": pFunctionName, "P_REQUEST_TYPE": requestType}; postParams.addAll(AppState().postParamsJson); - return await ApiClient().postJsonForObject((json) { - GenericResponseModel? responseData = GenericResponseModel.fromJson(json); - return responseData; - }, url, postParams); + return await ApiClient().postJsonForObject( + (json) { + GenericResponseModel? responseData = GenericResponseModel.fromJson(json); + return responseData; + }, + url, + postParams, + ); } Future getValueSetValues(String pSegmentName, String pDescFlexContextCode, String pDescFlexName, List> list) async { @@ -142,41 +206,38 @@ class ProfileApiClient { "GetValueSetValuesTBL": list, }; postParams.addAll(AppState().postParamsJson); - return await ApiClient().postJsonForObject((json) { - GenericResponseModel? responseData = GenericResponseModel.fromJson(json); - return responseData.getValueSetValuesList!.first; - }, url, postParams); + return await ApiClient().postJsonForObject( + (json) { + GenericResponseModel? responseData = GenericResponseModel.fromJson(json); + return responseData.getValueSetValuesList!.first; + }, + url, + postParams, + ); } Future> getPhoneNumberTypes() async { String url = "${ApiConsts.erpRest}GET_OBJECT_VALUES"; Map postParams = {"P_MENU_TYPE": "E", "P_SELECTED_RESP_ID": -999, "P_OBJECT_NAME": "PHONE_TYPE", "P_OBJECT_TYPE": "LOOKUP"}; postParams.addAll(AppState().postParamsJson); - return await ApiClient().postJsonForObject((json) { - GenericResponseModel? responseData = GenericResponseModel.fromJson(json); - return responseData.getObjectValuesList ?? []; - }, url, postParams); + return await ApiClient().postJsonForObject( + (json) { + GenericResponseModel? responseData = GenericResponseModel.fromJson(json); + return responseData.getObjectValuesList ?? []; + }, + url, + postParams, + ); } - Future submetConatctTransaction( - String pFunctionName, - String actionType, - int contactRelationId, - String date, - ) async { + Future submetConatctTransaction(String pFunctionName, String actionType, int contactRelationId, String date) async { String url = "${ApiConsts.erpRest}SUBMIT_CONTACT_TRANSACTION"; Map postParams = { "P_MENU_TYPE": "E", "P_SELECTED_RESP_ID": -999, "P_FUNCTION_NAME": pFunctionName, "EITTransactionTBL": [ - { - "TRANSACTION_NUMBER": 1, - "NAME": "DATE_END", - "VARCHAR2_VALUE": null, - "NUMBER_VALUE": null, - "DATE_VALUE": date, - } + {"TRANSACTION_NUMBER": 1, "NAME": "DATE_END", "VARCHAR2_VALUE": null, "NUMBER_VALUE": null, "DATE_VALUE": date}, ], "P_CONTACT_RELATIONSHIP_ID": contactRelationId, "P_ACTION": actionType, @@ -184,28 +245,29 @@ class ProfileApiClient { "LegislationCodeStr": "SA", }; postParams.addAll(AppState().postParamsJson); - return await ApiClient().postJsonForObject((json) { - GenericResponseModel? responseData = GenericResponseModel.fromJson(json); - return responseData.submitContactTransactionList; - }, url, postParams); + return await ApiClient().postJsonForObject( + (json) { + GenericResponseModel? responseData = GenericResponseModel.fromJson(json); + return responseData.submitContactTransactionList; + }, + url, + postParams, + ); } Future getApproves(String action, String comments, int transactionId, String itemKey) async { String url = "${ApiConsts.erpRest}GET_APPROVERS_LIST"; - Map postParams = { - "P_AME_TRANSACTION_TYPE": "SSHRMS", - "P_PAGE_LIMIT": 1000, - "P_PAGE_NUM": 1, - "P_TRANSACTION_ID": transactionId, - "P_ACTION_MODE": action, - "P_COMMENTS": comments, - }; + Map postParams = {"P_AME_TRANSACTION_TYPE": "SSHRMS", "P_PAGE_LIMIT": 1000, "P_PAGE_NUM": 1, "P_TRANSACTION_ID": transactionId, "P_ACTION_MODE": action, "P_COMMENTS": comments}; postParams.addAll(AppState().postParamsJson); - return await ApiClient().postJsonForObject((json) { - GenericResponseModel? responseData = GenericResponseModel.fromJson(json); - // return responseData.getApprovesList; - return (responseData.getApprovesList?.length ?? 0) > 0 ? responseData.getApprovesList!.first : null; - }, url, postParams); + return await ApiClient().postJsonForObject( + (json) { + GenericResponseModel? responseData = GenericResponseModel.fromJson(json); + // return responseData.getApprovesList; + return (responseData.getApprovesList?.length ?? 0) > 0 ? responseData.getApprovesList!.first : null; + }, + url, + postParams, + ); } Future submitPhoneNumbers(List empList) async { @@ -214,50 +276,49 @@ class ProfileApiClient { "P_MENU_TYPE": "E", "P_SELECTED_RESP_ID": -999, "P_FUNCTION_NAME": "HR_PERINFO_SS", - "P_MBL_PHONES_TBL": empList.map((element) { - return element.toJson(); - }).toList() + "P_MBL_PHONES_TBL": + empList.map((element) { + return element.toJson(); + }).toList(), }; postParams.addAll(AppState().postParamsJson); - return await ApiClient().postJsonForObject((json) { - GenericResponseModel? responseData = GenericResponseModel.fromJson(json); - return SubmitPhonesTransactionList.fromJson(responseData.submitPhonesTransactionList ?? {}); - }, url, postParams); + return await ApiClient().postJsonForObject( + (json) { + GenericResponseModel? responseData = GenericResponseModel.fromJson(json); + return SubmitPhonesTransactionList.fromJson(responseData.submitPhonesTransactionList ?? {}); + }, + url, + postParams, + ); } Future startPhoneApprovalProcess(String action, String comments, String itemKey, int transactionId) async { String url = "${ApiConsts.erpRest}START_PHONES_APPROVAL_PROCESS"; - Map postParams = { - "P_SELECTED_RESP_ID": -999, - "P_MENU_TYPE": "E", - "P_ACTION_MODE": action, - "P_COMMENTS": comments, - "P_ITEM_KEY": itemKey, - "P_TRANSACTION_ID": transactionId, - }; + Map postParams = {"P_SELECTED_RESP_ID": -999, "P_MENU_TYPE": "E", "P_ACTION_MODE": action, "P_COMMENTS": comments, "P_ITEM_KEY": itemKey, "P_TRANSACTION_ID": transactionId}; postParams.addAll(AppState().postParamsJson); - return await ApiClient().postJsonForObject((json) { - GenericResponseModel? responseData = GenericResponseModel.fromJson(json); - return responseData.startPhonesApprovalProcessList; - }, url, postParams); + return await ApiClient().postJsonForObject( + (json) { + GenericResponseModel? responseData = GenericResponseModel.fromJson(json); + return responseData.startPhonesApprovalProcessList; + }, + url, + postParams, + ); } Future startAddressApprovalProcess(String action, String comments, String itemKey, int transactionId) async { String url = "${ApiConsts.erpRest}START_ADDRESS_APPROVAL_PROCESS"; - Map postParams = { - "P_SELECTED_RESP_ID": -999, - "P_MENU_TYPE": "E", - "P_ACTION_MODE": action, - "P_COMMENTS": comments, - "P_ITEM_KEY": itemKey, - "P_TRANSACTION_ID": transactionId, - }; + Map postParams = {"P_SELECTED_RESP_ID": -999, "P_MENU_TYPE": "E", "P_ACTION_MODE": action, "P_COMMENTS": comments, "P_ITEM_KEY": itemKey, "P_TRANSACTION_ID": transactionId}; postParams.addAll(AppState().postParamsJson); - return await ApiClient().postJsonForObject((json) { - GenericResponseModel? responseData = GenericResponseModel.fromJson(json); - return responseData.startAddressApprovalProcessList; - }, url, postParams); + return await ApiClient().postJsonForObject( + (json) { + GenericResponseModel? responseData = GenericResponseModel.fromJson(json); + return responseData.startAddressApprovalProcessList; + }, + url, + postParams, + ); } Future submitBasicDetailsTransaction(String pDescFlexContextCode, String pFunctionName, List> list, int correctOrNew) async { @@ -273,31 +334,38 @@ class ProfileApiClient { postParams.addAll(AppState().postParamsJson); postParams["EITTransactionTBL"] = list; postParams["EITTransactionTBLModel"] = list; - return await ApiClient().postJsonForObject((json) { - GenericResponseModel responseData = GenericResponseModel.fromJson(json); - return responseData.submitBasicDetTransactionList!; //ESERVICESDV.fromJson(responseData.getDefaultValueList!.toJson()); - }, url, postParams); + return await ApiClient().postJsonForObject( + (json) { + GenericResponseModel responseData = GenericResponseModel.fromJson(json); + return responseData.submitBasicDetTransactionList!; //ESERVICESDV.fromJson(responseData.getDefaultValueList!.toJson()); + }, + url, + postParams, + ); } Future startBasicDetailsApprovalProcess(String action, String comments, String itemKey, int transactionId) async { String url = "${ApiConsts.erpRest}START_BASIC_DET_APPR_PROCESS"; - Map postParams = { - "P_SELECTED_RESP_ID": -999, - "P_MENU_TYPE": "E", - "P_ACTION_MODE": action, - "P_COMMENTS": comments, - "P_ITEM_KEY": itemKey, - "P_TRANSACTION_ID": transactionId, - }; + Map postParams = {"P_SELECTED_RESP_ID": -999, "P_MENU_TYPE": "E", "P_ACTION_MODE": action, "P_COMMENTS": comments, "P_ITEM_KEY": itemKey, "P_TRANSACTION_ID": transactionId}; postParams.addAll(AppState().postParamsJson); - return await ApiClient().postJsonForObject((json) { - GenericResponseModel? responseData = GenericResponseModel.fromJson(json); - return responseData.startAddressApprovalProcessList; - }, url, postParams); + return await ApiClient().postJsonForObject( + (json) { + GenericResponseModel? responseData = GenericResponseModel.fromJson(json); + return responseData.startAddressApprovalProcessList; + }, + url, + postParams, + ); } Future submitAddressTransaction( - String pDescFlexContextCode, String pFunctionName, List> list, int correctOrNew, String countryCode, String effectiveDate) async { + String pDescFlexContextCode, + String pFunctionName, + List> list, + int correctOrNew, + String countryCode, + String effectiveDate, + ) async { String url = "${ApiConsts.erpRest}SUBMIT_ADDRESS_TRANSACTION"; Map postParams = { "P_SELECTED_RESP_ID": -999, @@ -314,23 +382,28 @@ class ProfileApiClient { postParams.addAll(AppState().postParamsJson); postParams["EITTransactionTBL"] = list; // postParams["EITTransactionTBLModel"] = list; - return await ApiClient().postJsonForObject((json) { - GenericResponseModel responseData = GenericResponseModel.fromJson(json); - return responseData.submitAddressTransactionList!; //ESERVICESDV.fromJson(responseData.getDefaultValueList!.toJson()); - }, url, postParams); + return await ApiClient().postJsonForObject( + (json) { + GenericResponseModel responseData = GenericResponseModel.fromJson(json); + return responseData.submitAddressTransactionList!; //ESERVICESDV.fromJson(responseData.getDefaultValueList!.toJson()); + }, + url, + postParams, + ); } Future> getCountriesList() async { String url = "${ApiConsts.erpRest}GET_COUNTRIES"; - Map postParams = { - "P_MENU_TYPE": "E", - "P_SELECTED_RESP_ID": -999, - }; + Map postParams = {"P_MENU_TYPE": "E", "P_SELECTED_RESP_ID": -999}; postParams.addAll(AppState().postParamsJson); - return await ApiClient().postJsonForObject((json) { - GenericResponseModel? responseData = GenericResponseModel.fromJson(json); - return responseData.getCountriesList ?? []; - }, url, postParams); + return await ApiClient().postJsonForObject( + (json) { + GenericResponseModel? responseData = GenericResponseModel.fromJson(json); + return responseData.getCountriesList ?? []; + }, + url, + postParams, + ); } // add & update family member @@ -338,73 +411,75 @@ class ProfileApiClient { String url = "${ApiConsts.erpRest}GET_BASIC_DET_DFF_STRUCTURE"; Map postParams = {"P_FUNCTION_NAME": "HR_PERINFO_SS", "P_MENU_TYPE": "E", "P_REQUEST_TYPE": "CONTACT", "P_SELECTED_RESP_ID": -999}; postParams.addAll(AppState().postParamsJson); - return await ApiClient().postJsonForObject((json) { - GenericResponseModel? responseData = GenericResponseModel.fromJson(json); - return responseData.getBasicDetDffStructureList ?? []; - }, url, postParams); + return await ApiClient().postJsonForObject( + (json) { + GenericResponseModel? responseData = GenericResponseModel.fromJson(json); + return responseData.getBasicDetDffStructureList ?? []; + }, + url, + postParams, + ); } // add & update family member Future> getContactColsStructureList(String actionType) async { String url = "${ApiConsts.erpRest}GET_CONTACT_COLS_STRUCTURE"; - Map postParams = { - "P_ACTION": actionType, - "P_FUNCTION_NAME": "HR_PERINFO_SS", - }; + Map postParams = {"P_ACTION": actionType, "P_FUNCTION_NAME": "HR_PERINFO_SS"}; postParams.addAll(AppState().postParamsJson); - return await ApiClient().postJsonForObject((json) { - GenericResponseModel? responseData = GenericResponseModel.fromJson(json); - return responseData.getContactColsStructureList ?? []; - }, url, postParams); + return await ApiClient().postJsonForObject( + (json) { + GenericResponseModel? responseData = GenericResponseModel.fromJson(json); + return responseData.getContactColsStructureList ?? []; + }, + url, + postParams, + ); } // update family member Future> getContactDetailsList(int contactRelationId) async { String url = "${ApiConsts.erpRest}GET_CONTACT_DETAILS"; - Map postParams = { - "P_MENU_TYPE": "E", - "P_SELECTED_RESP_ID": -999, - "P_CONTACT_RELATIONSHIP_ID": contactRelationId, - }; + Map postParams = {"P_MENU_TYPE": "E", "P_SELECTED_RESP_ID": -999, "P_CONTACT_RELATIONSHIP_ID": contactRelationId}; postParams.addAll(AppState().postParamsJson); - return await ApiClient().postJsonForObject((json) { - GenericResponseModel? responseData = GenericResponseModel.fromJson(json); - return responseData.getContactDetailsList ?? []; - }, url, postParams); + return await ApiClient().postJsonForObject( + (json) { + GenericResponseModel? responseData = GenericResponseModel.fromJson(json); + return responseData.getContactDetailsList ?? []; + }, + url, + postParams, + ); } // add & update family member Future> getContactDffStructureList() async { String url = "${ApiConsts.erpRest}GET_CONTACT_DFF_STRUCTURE"; - Map postParams = { - "P_DESC_FLEX_CONTEXT_CODE": "HMG_ADD_CONTACT_DETAILS", - "P_FUNCTION_NAME": "HR_PERINFO_SS", - "P_MENU_TYPE": "E", - "P_SELECTED_RESP_ID": -999, - }; + Map postParams = {"P_DESC_FLEX_CONTEXT_CODE": "HMG_ADD_CONTACT_DETAILS", "P_FUNCTION_NAME": "HR_PERINFO_SS", "P_MENU_TYPE": "E", "P_SELECTED_RESP_ID": -999}; postParams.addAll(AppState().postParamsJson); - return await ApiClient().postJsonForObject((json) { - GenericResponseModel? responseData = GenericResponseModel.fromJson(json); - return responseData.getContactDffStructureList ?? []; - }, url, postParams); + return await ApiClient().postJsonForObject( + (json) { + GenericResponseModel? responseData = GenericResponseModel.fromJson(json); + return responseData.getContactDffStructureList ?? []; + }, + url, + postParams, + ); } Future submitContactTransactionAddAndUpdate(String actionType, relationId, List> list) async { String url = "${ApiConsts.erpRest}SUBMIT_CONTACT_TRANSACTION"; - Map postParams = { - "P_MENU_TYPE": "E", - "P_SELECTED_RESP_ID": -999, - "P_FUNCTION_NAME": "HR_PERINFO_SS", - "P_ACTION": actionType, - "P_CONTACT_RELATIONSHIP_ID": relationId, - }; + Map postParams = {"P_MENU_TYPE": "E", "P_SELECTED_RESP_ID": -999, "P_FUNCTION_NAME": "HR_PERINFO_SS", "P_ACTION": actionType, "P_CONTACT_RELATIONSHIP_ID": relationId}; postParams.addAll(AppState().postParamsJson); postParams["EITTransactionTBL"] = list; - return await ApiClient().postJsonForObject((json) { - GenericResponseModel? responseData = GenericResponseModel.fromJson(json); - return responseData.submitContactTransactionList; - }, url, postParams); + return await ApiClient().postJsonForObject( + (json) { + GenericResponseModel? responseData = GenericResponseModel.fromJson(json); + return responseData.submitContactTransactionList; + }, + url, + postParams, + ); } Future> getPerformanceAppraisal() async { @@ -412,9 +487,91 @@ class ProfileApiClient { Map postParams = {}; postParams.addAll(AppState().postParamsJson); - return await ApiClient().postJsonForObject((json) { - GenericResponseModel? responseData = GenericResponseModel.fromJson(json); - return responseData.getPerformanceAppraisalList ?? []; - }, url, postParams); + return await ApiClient().postJsonForObject( + (json) { + GenericResponseModel? responseData = GenericResponseModel.fromJson(json); + return responseData.getPerformanceAppraisalList ?? []; + }, + url, + postParams, + ); + } + + Future getPatientInfo({required String nationalId, required String dob, String? logInTokenID}) async { + // NHIC Service GetPatientInfo only works on Production (hmgwebservices.com) + // String url = "https://hmgwebservices.com/Services/NHIC.svc/REST/GetPatientInfo"; + String url = "${ApiConsts.erpRest}GetMemberInfo"; + String token = logInTokenID ?? AppState().postParamsObject?.logInTokenID ?? ""; + + Map postParams = { + "PatientIdentificationID": int.tryParse(nationalId.trim()) ?? nationalId.trim(), + "PatientOutSA": 0, + "DOB": dob.trim(), + "LogInTokenID": token, + "PatientMobileNumber": AppState().postParamsObject?.mobileNumber, // added this as per e-services changed + "TokenID": AppState().postParamsObject?.tokenID, // added this as per e-services changed + "LanguageID": AppState().postParamsObject?.languageID, // added this as per e-services changed + "Channel": AppState().postParamsObject?.channel, + "VersionID": AppState().postParamsObject?.versionID, + "UserName": AppState().postParamsObject?.userName, + }; + print(jsonEncode(postParams)); + return await ApiClient().postJsonForObject( + (json) { + return PatientInfoModel.fromJson(json); + }, + url, + postParams, + ); + } + + Future processContactMazayaAccess({ + required String accessAction, + int? contactRelationshipId, + int? contactPersonId, + int? personId, + // Same shape as the NHIC-style patient-info call: PatientIdentificationID is the + // dependent's national ID (GetEmployeeContactsList.nATIONALIDENTIFIER), PatientOutSA is + // always 0, and the other two are passed straight through from that same contact's + // P_BIRTH_DATE / P_NHIC_VERIFIED_FLAG response fields. + String? patientIdentificationId, + patientBirthDate, + String? nhicVerifiedFlag, + }) async { + String url = "${ApiConsts.erpRest}PROCESS_CONTACT_MAZAYA_ACCESS"; + String action = accessAction.trim().toLowerCase() == "revoke" ? "Revoke" : "Grant"; + bool isGrant = action == "Grant"; + Map postParams = {"P_MENU_TYPE": "E", "P_SELECTED_RESP_ID": -999, "P_ACCESS_ACTION": action}; + if (contactRelationshipId != null) { + postParams["P_CONTACT_RELATIONSHIP_ID"] = contactRelationshipId; + } + if (contactPersonId != null) { + postParams["P_CONTACT_PERSON_ID"] = contactPersonId; + } + if (personId != null) { + postParams["P_PERSON_ID"] = personId; + } + postParams["PatientIdentificationID"] = patientIdentificationId?.replaceAll('-', '').trim(); + // Only the Grant call carries these - Revoke's payload stays exactly as it was before + // this feature. P_BIRTH_DATE/P_NHIC_VERIFIED_FLAG are always sent (even as null, if the + // source contact from GET_EMPLOYEE_CONTACTS didn't have a value) once we're granting. + if (isGrant) { + postParams["PatientOutSA"] = 0; + postParams["P_BIRTH_DATE"] = _toWcfJsonDate(patientBirthDate); + postParams["P_NHIC_VERIFIED_FLAG"] = nhicVerifiedFlag?.trim(); + if (patientIdentificationId != null && patientIdentificationId.trim().isNotEmpty) { + // National ID comes formatted with dashes (e.g. "2-5616-1545-7") - the backend wants + // the plain digit string (e.g. "2561615457"). + + } + } + postParams.addAll(AppState().postParamsJson); + return await ApiClient().postJsonForObject( + (json) { + return GenericResponseModel.fromJson(json); + }, + url, + postParams, + ); } } diff --git a/lib/app_state/app_state.dart b/lib/app_state/app_state.dart index 818d91f..35fb69a 100644 --- a/lib/app_state/app_state.dart +++ b/lib/app_state/app_state.dart @@ -3,9 +3,11 @@ import 'dart:io'; import 'package:easy_localization/easy_localization.dart'; import 'package:mohem_flutter_app/models/chat/get_user_login_token_model.dart'; import 'package:mohem_flutter_app/models/itg_forms_models/request_detail_model.dart'; +import 'package:mohem_flutter_app/models/generic_response_model.dart'; import 'package:mohem_flutter_app/models/member_information_list_model.dart'; import 'package:mohem_flutter_app/models/member_login_list_model.dart'; import 'package:mohem_flutter_app/models/my_team/get_employee_subordinates_list.dart'; +import 'package:mohem_flutter_app/models/offers_and_discounts/wala_plus_city_model.dart'; import 'package:mohem_flutter_app/models/post_params_model.dart'; import 'package:mohem_flutter_app/models/privilege_list_model.dart'; import 'package:mohem_flutter_app/models/worklist_response_model.dart'; @@ -92,11 +94,72 @@ class AppState { final PostParamsModel _postParamsInitConfig = PostParamsModel(channel: 31, versionID: 10.1, mobileType: Platform.isAndroid ? "android" : "ios"); + PostParamsModel get postParamsInitConfig { + if (_postParamsInitConfig.pLegislationCode == null) { + if (_postParams?.pLegislationCode != null) { + _postParamsInitConfig.pLegislationCode = _postParams?.pLegislationCode; + } else if (_memberLoginList?.pLEGISLATIONCODE != null) { + _postParamsInitConfig.pLegislationCode = _memberLoginList?.pLEGISLATIONCODE; + } + } + return _postParamsInitConfig; + } + + GetWalaplusDepInfoModel? _walaplusDepInfoModel; + + GetWalaplusDepInfoModel? get walaplusDepInfoModel => _walaplusDepInfoModel; + + set setWalaplusDepInfoModel(GetWalaplusDepInfoModel? model) => _walaplusDepInfoModel = model; + + String? _dependentName; + + String? get dependentName => _dependentName; + + set setDependentName(String? name) => _dependentName = name; + + int? _dependentLoginType; + + int? get dependentLoginType => _dependentLoginType; + + set setDependentLoginType(int? type) => _dependentLoginType = type; + + String? _dependentDob; + + String? get dependentDob => _dependentDob; + + set setDependentDob(String? dob) => _dependentDob = dob; + + String? _authenticationTokenID; + + String? get authenticationTokenID => _authenticationTokenID; + + set setAuthenticationTokenID(String? token) => _authenticationTokenID = token; + + bool? _isEmpLastLoginToDevice; + + bool? get isEmpLastLoginToDevice => _isEmpLastLoginToDevice; + + set setIsEmpLastLoginToDevice(bool? val) => _isEmpLastLoginToDevice = val; + + WalaPlusCityModel? _selectedMazayaCity; + + WalaPlusCityModel? get selectedMazayaCity => _selectedMazayaCity; + + set setSelectedMazayaCity(WalaPlusCityModel? city) => _selectedMazayaCity = city; + void setPostParamsInitConfig() { isAuthenticated = false; isLogged = false; _memberLoginList = null; _memberInformationList = null; + _walaplusDepInfoModel = null; + _dependentName = null; + _dependentLoginType = null; + _dependentDob = null; + _authenticationTokenID = null; + _isEmpLastLoginToDevice = null; + _selectedMazayaCity = null; + _postParamsInitConfig.pLegislationCode = null; _postParams = _postParamsInitConfig; } @@ -110,9 +173,23 @@ class AppState { this._postParams = _postParams; } - bool isArabic(context) => EasyLocalization.of(context)?.locale.languageCode == "ar"; + bool isArabic(context) { + try { + if (context != null) { + return EasyLocalization.of(context)?.locale.languageCode == "ar"; + } + } catch (_) {} + return false; + } - int getLanguageID(context) => EasyLocalization.of(context)?.locale.languageCode == "ar" ? 1 : 2; + int getLanguageID(context) { + try { + if (context != null) { + return EasyLocalization.of(context)?.locale.languageCode == "ar" ? 1 : 2; + } + } catch (_) {} + return 2; + } String? _username; @@ -129,7 +206,13 @@ class AppState { MemberLoginListModel? get memberLoginList => _memberLoginList; - set setMemberLoginListModel(MemberLoginListModel? _memberLoginList) => this._memberLoginList = _memberLoginList; + set setMemberLoginListModel(MemberLoginListModel? _memberLoginList) { + this._memberLoginList = _memberLoginList; + if (_memberLoginList?.pLEGISLATIONCODE != null) { + _postParamsInitConfig.pLegislationCode = _memberLoginList?.pLEGISLATIONCODE?.trim().toUpperCase(); + _postParams?.pLegislationCode = _memberLoginList?.pLEGISLATIONCODE?.trim().toUpperCase(); + } + } MemberInformationListModel? _memberInformationList; diff --git a/lib/classes/app_permissions.dart b/lib/classes/app_permissions.dart index 1509b57..875a2be 100644 --- a/lib/classes/app_permissions.dart +++ b/lib/classes/app_permissions.dart @@ -1,5 +1,5 @@ import 'package:mohem_flutter_app/generated/locale_keys.g.dart'; -import 'package:mohem_flutter_app/widgets/dialogs/confirm_dialog.dart'; +import 'package:mohem_flutter_app/widgets/bottom_sheets/confirm_bottom_sheet.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:geolocator/geolocator.dart'; @@ -18,20 +18,18 @@ class AppPermissions { } static void showErrorLocationDialog(bool isPermissionError, BuildContext context) { - showDialog( - context: context, - builder: (cxt) => ConfirmDialog( - message: "Please provide location permission", - onTap: () { - if (isPermissionError) { - Geolocator.openAppSettings(); - } else { - Geolocator.openLocationSettings(); - } - Navigator.pop(context); - // createVacationRule(list); - }, - ), + ConfirmBottomSheet.show( + context, + message: LocaleKeys.pleaseProvideLocationPermission.tr(), + confirmText: LocaleKeys.openSettings.tr(), + showCancelButton: false, + onConfirm: () { + if (isPermissionError) { + Geolocator.openAppSettings(); + } else { + Geolocator.openLocationSettings(); + } + }, ); } diff --git a/lib/classes/consts.dart b/lib/classes/consts.dart index 79e0430..001d9ba 100644 --- a/lib/classes/consts.dart +++ b/lib/classes/consts.dart @@ -1,13 +1,13 @@ import 'package:mohem_flutter_app/ui/marathon/widgets/question_card.dart'; class ApiConsts { - // static String baseUrl = "http://10.200.204.11"; // Local server + // static String baseUrl = "http://10.201.214.101:2027"; // Local server // static String baseUrl = "https://erptstapp.srca.org.sa"; // SRCA server - // static String baseUrl = "https://uat.hmgwebservices.com"; // UAT ser343622ver - // static String baseUrl = "http://10.201.204.101:2024"; - // static String baseUrl = "https://webservices.hmg.com"; // PreProd - // static String baseUrl = "https://hmgwebservices.com"; // Live server + // static String baseUrl = "https://uat.hmgwebservices.com"; // UAT server + // static String baseUrl = "http://10.201.204.101:2024"; + // static String baseUrl = "https://webservices.hmg.com"; // PreProd + // static String baseUrl = "https://hmgwebservices.com"; // Live server // static String baseUrl = "https://mohemm.hmg.com"; // New Live server static String ssoBaseUrl = "https://sso.hmg.com"; // New Live server @@ -17,25 +17,43 @@ class ApiConsts { // static String baseUrl = "http://10.20.200.111:1010/"; // static String baseUrl = "https://mohemm.hmg.com"; // New Live server //static String eLearningBaseUrl = "https://elearning.hmg.com/moodle_dev/api/"; // Elearning server UAT - static String eLearningBaseUrl = "https://elearning.hmg.com/moodle/api/"; // Elearning server live - static String enSATLiveURL = "https://ensat.hmg.com/auth/autologin"; // ENSAT server live + static String eLearningBaseUrl = + "https://elearning.hmg.com/moodle/api/"; // Elearning server live + static String enSATLiveURL = + "https://ensat.hmg.com/auth/autologin"; // ENSAT server live // static String baseUrl = "http://10.20.200.111:1010/"; - // static String baseUrl = "https://webservices.hmg.com"; // PreProd - // static String baseUrl = "https://mohemm.hmg.com"; + // static String baseUrl = "https://webservices.hmg.com"; // PreProd + // static String baseUrl = "https://mohemm.hmg.com"; // static String baseUrl = "https://hmgwebservices.com"; // Live server static String baseUrlServices = baseUrl + "/Services/"; // server // static String baseUrlServices = "https://api.cssynapses.com/tangheem/"; // Live server static String utilitiesRest = baseUrlServices + "Utilities.svc/REST/"; - static String authenticationRest = baseUrlServices + "Authentication.svc/REST/"; + static String authenticationRest = + baseUrlServices + "Authentication.svc/REST/"; static String erpRest = baseUrlServices + "ERP.svc/REST/"; static String swpRest = baseUrlServices + "SWP.svc/REST/"; static String user = baseUrlServices + "api/User/"; static String cocRest = baseUrlServices + "COCWS.svc/REST/"; - static String ssoAuthRedirection = "https://sso-uat.hmg.com/api/auth/connect"; + // Dependent Login Endpoints + static String memberLoginDependent = erpRest + "MemberLogin_Dependent"; + static String mohemmSendOTPDependent = erpRest + "Mohemm_SendOTP_Dependent"; + static String mohemmCheckOTPDependent = erpRest + "Mohemm_CheckOTP_Dependent"; + + // WalaPlus Endpoints + static String walaPlusCategories = erpRest + "WalaPlus_Categories"; + static String walaPlusRetailers = erpRest + "WalaPlus_Retailers"; + static String walaPlusRetailersByID = erpRest + "WalaPlus_RetailersByID"; + static String walaPlusOfferRedeem = erpRest + "WalaPlus_OfferRedeem"; + static String walaPlusFavoritesAdd = erpRest + "WalaPlus_FavoritesAdd"; + static String walaPlusFavoritesRemove = erpRest + "WalaPlus_FavoritesRemove"; + static String walaPlusCities = erpRest + "WalaPlus_Cities"; + static String processContactMazayaAccess = + erpRest + "PROCESS_CONTACT_MAZAYA_ACCESS"; + static String ssoAuthRedirection = "https://sso-uat.hmg.com/api/auth/connect"; //Chat static String chatServerBaseUrl = "https://apiderichat.hmg.com/"; static String chatServerBaseApiUrl = chatServerBaseUrl + "api/"; @@ -43,16 +61,24 @@ class ApiConsts { static String chatHubConnectionUrl = chatServerBaseUrl + "ConnectionChatHub"; //Groups - static String getGroupByUserId = chatServerBaseApiUrl + "group/getgroupsbyuserid/"; - static String deleteGroup = chatServerBaseApiUrl + "group/updateGroupIsDeleted/"; - static String updateGroupAdmin = chatServerBaseApiUrl + "group/updateGroupAdmin/"; - static String getGroupChatHistoryAsync = chatServerBaseApiUrl + "GroupChat/GetGroupChatHistoryAsync/"; - static String addGroupsAndUsers = chatServerBaseApiUrl + "group/addgroupandusers/"; - static String updateGroupsAndUsers = chatServerBaseApiUrl + "group/updategroupandusers/"; + static String getGroupByUserId = + chatServerBaseApiUrl + "group/getgroupsbyuserid/"; + static String deleteGroup = + chatServerBaseApiUrl + "group/updateGroupIsDeleted/"; + static String updateGroupAdmin = + chatServerBaseApiUrl + "group/updateGroupAdmin/"; + static String getGroupChatHistoryAsync = + chatServerBaseApiUrl + "GroupChat/GetGroupChatHistoryAsync/"; + static String addGroupsAndUsers = + chatServerBaseApiUrl + "group/addgroupandusers/"; + static String updateGroupsAndUsers = + chatServerBaseApiUrl + "group/updategroupandusers/"; // static String chatSearchMember = chatLoginTokenUrl + "user/"; - static String chatRecentUrl = chatServerBaseApiUrl + "UserChatHistory/"; //For a Mem - static String chatSingleUserHistoryUrl = chatServerBaseApiUrl + "UserChatHistory/"; + static String chatRecentUrl = + chatServerBaseApiUrl + "UserChatHistory/"; //For a Mem + static String chatSingleUserHistoryUrl = + chatServerBaseApiUrl + "UserChatHistory/"; static String chatMediaImageUploadUrl = chatServerBaseApiUrl + "shared/"; static String chatFavUser = chatServerBaseApiUrl + "FavUser/"; static String chatUserImages = chatServerBaseUrl + "empservice/api/employee/"; @@ -64,25 +90,63 @@ class ApiConsts { static String marathonBaseUrl = marathonBaseUrlLive; // static String marathonBaseUrl = marathonBaseUrlUAT; static String marathonBaseUrlServices = "https://marathoon.com/service/"; - static String marathonParticipantLoginUrl = marathonBaseUrl + "auth/participantlogin"; + static String marathonParticipantLoginUrl = + marathonBaseUrl + "auth/participantlogin"; static String marathonProjectGetUrl = marathonBaseUrl + "Project/Project_Get"; static String marathonUpcomingUrl = marathonBaseUrl + "marathon/upcoming/"; - static String marathonJoinParticipantUrl = marathonBaseUrl + "participant/join"; + static String marathonJoinParticipantUrl = + marathonBaseUrl + "participant/join"; static String marathonNextQuestionUrl = marathonBaseUrl + "question/next"; static String marathonSubmitAnswerUrl = marathonBaseUrl + "question/submit"; static String marathonQualifiersUrl = marathonBaseUrl + "winner/getWinner/"; - static String marathonSelectedWinner = marathonBaseUrl + "winner/getSelectedWinner/"; - static String marathonGetMarathonersCount = marathonBaseUrl + "Participant/GetRemainingParticipants"; - static String marathonGetTutorial = marathonBaseUrl + "tutorial/GetTutorialNotification"; + static String marathonSelectedWinner = + marathonBaseUrl + "winner/getSelectedWinner/"; + static String marathonGetMarathonersCount = + marathonBaseUrl + "Participant/GetRemainingParticipants"; + static String marathonGetTutorial = + marathonBaseUrl + "tutorial/GetTutorialNotification"; //DummyCards for the UI static int tabletMinLength = 500; // Notification Center GUIDs for OTP - static String notificationCenterAppGUID = "VDZreDY5TTNmTDVudzc4aDkwU3ZqcW9OcmI0"; - static String notificationCenterLoginServiceGUID = "D8BD5C76-4B50-4122-81D0-C3AA25DB08A2"; - static String notificationCenterForgotPasswordServiceGUID = "5CCCC2C9-EEA0-4207-9BE9-EE1FBBFFE8D0"; + static String notificationCenterAppGUID = + "VDZreDY5TTNmTDVudzc4aDkwU3ZqcW9OcmI0"; + static String notificationCenterLoginServiceGUID = + "D8BD5C76-4B50-4122-81D0-C3AA25DB08A2"; + static String notificationCenterForgotPasswordServiceGUID = + "15AA0C70-8DA3-430B-8881-EAFBB508F296"; + + static MemberType selectedMemberType = MemberType.employee; + + static bool get ISDependetLogin => selectedMemberType == MemberType.dependent; + + static set ISDependetLogin(bool value) { + selectedMemberType = value ? MemberType.dependent : MemberType.employee; + } + + static bool get isDependetLogin => ISDependetLogin; + + static set isDependetLogin(bool value) => ISDependetLogin = value; +} + +enum MemberType { + employee(1), + dependent(2); + + final int value; + + const MemberType(this.value); + + bool get isEmployee => this == MemberType.employee; + + bool get isDependent => this == MemberType.dependent; + + static MemberType fromValue(int? value) { + if (value == 2) return MemberType.dependent; + return MemberType.employee; + } } class SharedPrefsConsts { diff --git a/lib/classes/inAppWebView.dart b/lib/classes/inAppWebView.dart index ccb24fd..d19f951 100644 --- a/lib/classes/inAppWebView.dart +++ b/lib/classes/inAppWebView.dart @@ -1,6 +1,7 @@ import 'dart:developer'; import 'package:flutter_inappwebview/flutter_inappwebview.dart'; +import 'package:permission_handler/permission_handler.dart'; class MyInAppBrowser extends InAppBrowser { final Function onExitCallback; @@ -30,4 +31,21 @@ class MyInAppBrowser extends InAppBrowser { void onLoadError(Uri? url, int code, String message) { log("Can't load $url.. Error: $message"); } + + @override + Future onPermissionRequest(PermissionRequest permissionRequest) async { + log("onPermissionRequest: ${permissionRequest.resources}"); + try { + var status = await Permission.camera.status; + if (!status.isGranted) { + await Permission.camera.request(); + } + } catch (e) { + log("Error checking/requesting camera permission: $e"); + } + return PermissionResponse( + resources: permissionRequest.resources, + action: PermissionResponseAction.GRANT, + ); + } } diff --git a/lib/classes/notifications.dart b/lib/classes/notifications.dart index 058606c..9769756 100644 --- a/lib/classes/notifications.dart +++ b/lib/classes/notifications.dart @@ -2,7 +2,6 @@ import 'dart:io'; import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; @@ -13,7 +12,6 @@ import 'package:mohem_flutter_app/classes/date_uitl.dart'; import 'package:mohem_flutter_app/classes/utils.dart'; import 'package:mohem_flutter_app/models/get_notifications_response_model.dart'; import 'package:mohem_flutter_app/ui/notifications/notification_details_page.dart'; -import 'package:permission_handler/permission_handler.dart'; final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); @@ -29,20 +27,14 @@ class AppNotifications { String _huaweiToken = ''; late BuildContext context; + bool _isInitialized = false; Future requestPermissions() async { if (Platform.isIOS) { await flutterLocalNotificationsPlugin.resolvePlatformSpecificImplementation()?.requestPermissions(alert: true, badge: true, sound: true); } else if (Platform.isAndroid) { - AndroidFlutterLocalNotificationsPlugin? androidImplementation = flutterLocalNotificationsPlugin.resolvePlatformSpecificImplementation(); - bool? granted = await androidImplementation?.requestNotificationsPermission(); - if (granted == false) { - if (kDebugMode) { - print("-------------------- Permission Granted ------------------------"); - print(granted); - } - await Permission.notification.request(); - } + AndroidFlutterLocalNotificationsPlugin? androidImplementation = flutterLocalNotificationsPlugin.resolvePlatformSpecificImplementation(); + await androidImplementation?.requestNotificationsPermission(); } } @@ -53,13 +45,11 @@ class AppNotifications { this.context = context; - await requestPermissions(); + if (!_isInitialized) { + _isInitialized = true; + await requestPermissions(); + } AppState().setDeviceToken = firebaseToken; - await Permission.notification.isDenied.then((bool value) { - if (value) { - Permission.notification.request(); - } - }); RemoteMessage? initialMessage = await FirebaseMessaging.instance.getInitialMessage(); if (initialMessage != null) _handleMessage(initialMessage); @@ -80,7 +70,7 @@ class AppNotifications { // await hmsApiAvailability.isHMSAvailable().then((value) async { if (!(await Utils.isGoogleServicesAvailable())) { huawei_push.Push.enableLogger(); - var result = await huawei_push.Push.setAutoInitEnabled(true); + await huawei_push.Push.setAutoInitEnabled(true); huawei_push.Push.onNotificationOpenedApp.listen((message) { // newMessage(toFirebaseRemoteMessage(message)); diff --git a/lib/classes/utils.dart b/lib/classes/utils.dart index ea12d8f..9e5bfb2 100644 --- a/lib/classes/utils.dart +++ b/lib/classes/utils.dart @@ -6,7 +6,6 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import 'package:fluttertoast/fluttertoast.dart'; import 'package:google_api_availability/google_api_availability.dart'; import 'package:mohem_flutter_app/app_state/app_state.dart'; import 'package:mohem_flutter_app/classes/colors.dart'; @@ -19,11 +18,13 @@ import 'package:mohem_flutter_app/generated/locale_keys.g.dart'; import 'package:mohem_flutter_app/provider/chat_provider_model.dart'; import 'package:mohem_flutter_app/widgets/dialogs/confirm_dialog.dart'; import 'package:mohem_flutter_app/widgets/loading_dialog.dart'; +import 'package:mohem_flutter_app/widgets/toast/app_toast.dart'; import 'package:nfc_manager/nfc_manager.dart'; import 'package:nfc_manager/nfc_manager_android.dart'; import 'package:nfc_manager/nfc_manager_ios.dart'; // import 'package:nfc_manager/platform_tags.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:mohem_flutter_app/ui/profile/mazaya_access_result_screen.dart'; // ignore_for_file: avoid_annotating_with_dynamic @@ -32,15 +33,25 @@ class Utils { static bool get isLoading => _isLoadingVisible; - static void showToast(String message, {bool longDuration = true}) { - Fluttertoast.showToast( - msg: message, - toastLength: longDuration ? Toast.LENGTH_LONG : Toast.LENGTH_SHORT, - gravity: ToastGravity.BOTTOM, - timeInSecForIosWeb: 1, - backgroundColor: Colors.black54, - textColor: Colors.white, - fontSize: 13.0, + // longDuration is accepted but no longer varies the duration - every caller already used the + // default (true), so AppToast.plainSeconds covers both cases with one fixed value. + // isSuccess: true -> soft green/green (same combo as dependent_details_screen.dart's "Active" + // badge); false (default) -> soft red/red (same combo as that screen's "Access Off" badge). + // Callers showing a value straight from an API response should pass isSuccess: response's + // MessageStatus == 1; everywhere else (validation messages, caught errors) just defaults to + // red, and the handful of hardcoded success confirmations pass isSuccess: true explicitly. + // onComplete makes this an action-toast (longer actionSeconds duration + icon shown) instead + // of a plain info toast - this is the single calling point, every caller should go through + // here instead of calling AppToast.show directly, so the color/duration/icon rules in one place. + static void showToast(String message, {bool longDuration = true, bool isSuccess = false, VoidCallback? onComplete}) { + AppToast.show( + message, + seconds: onComplete != null ? AppToast.actionSeconds : AppToast.plainSeconds, + showIcon: onComplete != null, + showCountdownText: true, + bgColor: isSuccess ? AppToast.successBgColor : AppToast.errorBgColor, + textColor: isSuccess ? AppToast.successTextColor : AppToast.errorTextColor, + onComplete: onComplete, ); } @@ -96,9 +107,57 @@ class Utils { return await prefs.setString(key, value); } + static bool isMazayaAccessRevoked(dynamic error) { + if (error == null) return false; + String str = ""; + if (error is APIException && error.error != null) { + String errCode = error.error?.errorCode?.toString() ?? ""; + String errMsg = error.error?.errorMessage?.toString() ?? ""; + str = "$errCode $errMsg ${error.message}"; + if (errCode.trim().toUpperCase() == "E4") { + return true; + } + } else { + str = error.toString(); + } + String lower = str.toLowerCase(); + if (lower.contains("no active grant") || + lower.contains("grant request") || + lower.contains("grant was found") || + lower.contains("grant_request") || + lower.contains("authorized to access") || + lower.contains("not authorized") || + lower.contains("e4:") || + lower.contains("e4 -") || + lower.contains("e4 :")) { + return true; + } + if (str.contains("طلب منح") || + str.contains("منح نشط") || + str.contains("لا يوجد طلب منح") || + str.contains("تم إلغاء وصول مزايا") || + str.contains("إلغاء مزايا") || + str.contains("غير مصرح لك بالوصول") || + str.contains("غير مصرح")) { + return true; + } + return false; + } + static void handleException(dynamic exception, cxt, Function(String)? onErrorMessage) { + if (isMazayaAccessRevoked(exception)) { + String depName = AppState().dependentName ?? ""; + if (depName.isEmpty) { + depName = AppState().walaplusDepInfoModel?.pDependentName ?? ""; + } + if (cxt != null) { + Navigator.push(cxt, MaterialPageRoute(builder: (context) => MazayaAccessResultScreen(isGranted: false, dependentName: depName))); + return; + } + } + String errorMessage; - if (exception.error.errorType != null && exception.error.errorType == 4) { + if (exception is APIException && exception.error?.errorType == 4) { Navigator.pushNamedAndRemoveUntil(cxt, AppRoutes.appUpdateScreen, (_) => false, arguments: exception.error?.errorMessage); } else { if (exception is APIException) { @@ -114,17 +173,11 @@ class Utils { onErrorMessage(errorMessage); } else { if (!AppState().isAuthenticated) { - showDialog( - barrierDismissible: false, - context: cxt, - builder: - (cxt) => ConfirmDialog( - message: errorMessage, - onTap: () { - Navigator.pushNamedAndRemoveUntil(cxt, AppRoutes.login, (Route route) => false); - }, - onCloseTap: () {}, - ), + showToast( + errorMessage, + onComplete: () { + Navigator.pushNamedAndRemoveUntil(cxt, AppRoutes.login, (Route route) => false); + }, ); } else { if (cxt != null) { @@ -142,7 +195,15 @@ class Utils { } static void confirmDialog(cxt, String message, {VoidCallback? onTap}) { - showDialog(context: cxt, builder: (BuildContext cxt) => ConfirmDialog(message: message, onTap: onTap)); + // No onTap means there's nothing to confirm - it's a plain notice, so a self-dismissing + // toast replaces the old "OK just closes" dialog instead of requiring a tap. Every current + // caller of this path passes an error/warning message (catch blocks, location/fake-location + // warnings, session errors) - never success - so red is correct here. + if (onTap == null) { + showToast(message); + } else { + showDialog(context: cxt, builder: (BuildContext cxt) => ConfirmDialog(message: message, onTap: onTap)); + } } static Widget getNoDataWidget(BuildContext context) { @@ -414,7 +475,9 @@ class Utils { AppState().isLogged = false; AppState().setPostParamsInitConfig(); if (chatData != null) { - chatData.disposeData(); + try { + chatData.disposeData(); + } catch (e) {} } // SharedPreferences prefs = await SharedPreferences.getInstance(); // await prefs.clear(); diff --git a/lib/config/routes.dart b/lib/config/routes.dart index c3376e9..35d248e 100644 --- a/lib/config/routes.dart +++ b/lib/config/routes.dart @@ -32,10 +32,15 @@ import 'package:mohem_flutter_app/ui/marathon/marathon_screen.dart'; import 'package:mohem_flutter_app/ui/marathon/marathon_sponsor_video_screen.dart'; import 'package:mohem_flutter_app/ui/marathon/marathon_tutorial_viewer_screen.dart'; import 'package:mohem_flutter_app/ui/marathon/marathon_waiting_screen.dart'; +import 'package:mohem_flutter_app/ui/mazaya/retailers_list_screen.dart'; +import 'package:mohem_flutter_app/ui/mazaya/mazaya_discover_screen.dart'; +import 'package:mohem_flutter_app/ui/mazaya/saved_retailers_screen.dart'; +import 'package:mohem_flutter_app/ui/mazaya/retailer_details_screen.dart'; import 'package:mohem_flutter_app/ui/misc/request_submit_screen.dart'; import 'package:mohem_flutter_app/ui/my_attendance/dynamic_screens/dynamic_input_screen.dart'; import 'package:mohem_flutter_app/ui/my_attendance/dynamic_screens/dynamic_listview_screen.dart'; import 'package:mohem_flutter_app/ui/my_attendance/services_menu_list_screen.dart'; + // import 'package:mohem_flutter_app/ui/my_attendance/my_attendance_screen.dart'; import 'package:mohem_flutter_app/ui/my_team/create_request.dart'; import 'package:mohem_flutter_app/ui/my_team/employee_details.dart'; @@ -48,6 +53,9 @@ import 'package:mohem_flutter_app/ui/parking_qr/parking_qr.dart'; import 'package:mohem_flutter_app/ui/parking_qr/parking_qr.dart'; import 'package:mohem_flutter_app/ui/payslip/monthly_pay_slip_screen.dart'; import 'package:mohem_flutter_app/ui/performance/performance_appraisal_screen.dart'; +import 'package:mohem_flutter_app/ui/profile/add_dependent_verify_screen.dart'; +import 'package:mohem_flutter_app/ui/profile/dependent_details_screen.dart'; +import 'package:mohem_flutter_app/ui/profile/mazaya_access_result_screen.dart'; import 'package:mohem_flutter_app/ui/profile/add_update_family_member.dart'; import 'package:mohem_flutter_app/ui/profile/basic_details.dart'; import 'package:mohem_flutter_app/ui/profile/contact_details.dart'; @@ -86,6 +94,7 @@ import 'package:mohem_flutter_app/ui/work_list/itg_detail_screen.dart'; import 'package:mohem_flutter_app/ui/work_list/work_list_screen.dart'; import 'package:mohem_flutter_app/ui/work_list/worklist_detail_screen.dart'; import 'package:mohem_flutter_app/ui/work_list/worklist_settings.dart'; +import 'package:mohem_flutter_app/widgets/app_in_app_webview_screen.dart'; class AppRoutes { static GlobalKey navigatorKey = GlobalKey(); @@ -100,6 +109,10 @@ class AppRoutes { static const String newPassword = "/newPassword"; static const String loginVerification = "/loginVerification"; static const String dashboard = "/dashboard"; + static const String retailersListScreen = "/dashboardScreenForDependet"; + static const String retailerDetailsScreen = "/walaPlusRetailerDetails"; + static const String mazayaDiscover = "/mazayaDiscover"; + static const String savedReyailersScreen = "/mazayaSaved"; static const String todayAttendance = "/todayAttendance"; static const String subMenuScreen = "/submenuScreen"; static const String addEitScreen = "/addeitScreen"; @@ -145,6 +158,9 @@ class AppRoutes { static const String familyMembers = "/familyMembers"; static const String deleteFamilyMember = "/deleteFamilyMember"; static const String addUpdateFamilyMember = "/addUpdateFamilyMember"; + static const String addDependentVerify = "/addDependentVerify"; + static const String dependentDetails = "/dependentDetails"; + static const String mazayaAccessResult = "/mazayaAccessResult"; // Mowadhafhi static const String mowadhafhi = "/mowadhafhi"; @@ -229,12 +245,24 @@ class AppRoutes { //Parking QR static const String parkingQr = "/parkingQrScreen"; + static const String appInAppWebView = "/appInAppWebView"; static final Map routes = { login: (BuildContext context) => LoginScreen(), verifyLogin: (BuildContext context) => VerifyLoginScreen(), verifyLastLogin: (BuildContext context) => VerifyLastLoginScreen(), dashboard: (BuildContext context) => DashboardScreen(), + retailersListScreen: (BuildContext context) => const RetailersListScreen(), + retailerDetailsScreen: (BuildContext context) => const RetailerDetailsScreen(), + mazayaDiscover: (BuildContext context) => const MazayaDiscoverScreen(), + savedReyailersScreen: (BuildContext context) => const SavedRetailersScreen(), + appInAppWebView: (BuildContext context) { + Object? args = ModalRoute.of(context)?.settings.arguments; + if (args is Map) { + return AppInAppWebViewScreen(url: args['url'] ?? "", title: args['title']); + } + return const Scaffold(); + }, survey: (BuildContext context) => SurveyScreen(), advertisement: (BuildContext context) => ITGAdsScreen(), @@ -284,6 +312,9 @@ class AppRoutes { deleteFamilyMember: (BuildContext context) => DeleteFamilyMember(ModalRoute.of(context)!.settings.arguments as int), requestSubmitScreen: (BuildContext context) => RequestSubmitScreen(), addUpdateFamilyMember: (BuildContext context) => AddUpdateFamilyMember(), + addDependentVerify: (BuildContext context) => const AddDependentVerifyScreen(), + dependentDetails: (BuildContext context) => const DependentDetailsScreen(), + mazayaAccessResult: (BuildContext context) => const MazayaAccessResultScreen(), //mowadhafhi mowadhafhi: (BuildContext context) => MowadhafhiHome(), @@ -350,13 +381,12 @@ class AppRoutes { unsafeDeviceScreen: (BuildContext context) => const UnsafeDeviceScreen(), appUpdateScreen: (BuildContext context) => const AppUpdateScreen(), childEducation: (BuildContext context) => ChildEducationAssistance(), - activityScreen:(BuildContext context) => const EventActivityScreen(), + activityScreen: (BuildContext context) => const EventActivityScreen(), //ETQAN OVR etqanOvr: (BuildContext context) => EtqanOvrHome(), etqanCreateRequest: (BuildContext context) => EtqanOvrCreateRequest(), etqanGetIncidientRequest: (BuildContext context) => EtqanOvrRequestDetailed(), - activityScreen:(BuildContext context) => const EventActivityScreen(), //Parking QR parkingQr: (BuildContext context) => const ParkingQrScreen(), diff --git a/lib/dialogs/otp_dialog.dart b/lib/dialogs/otp_dialog.dart index da2611e..5932f89 100644 --- a/lib/dialogs/otp_dialog.dart +++ b/lib/dialogs/otp_dialog.dart @@ -1,14 +1,17 @@ import 'dart:async'; +import 'dart:io'; import 'package:easy_localization/src/public_ext.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:mohem_flutter_app/api/login_api_client.dart'; import 'package:mohem_flutter_app/classes/colors.dart'; import 'package:mohem_flutter_app/extensions/int_extensions.dart'; import 'package:mohem_flutter_app/extensions/string_extensions.dart'; import 'package:mohem_flutter_app/generated/locale_keys.g.dart'; import 'package:mohem_flutter_app/widgets/button/default_button.dart'; import 'package:mohem_flutter_app/widgets/otp_widget.dart'; +import 'package:sms_otp_auto_verify/sms_otp_auto_verify.dart'; final ValueNotifier otpFieldClear = ValueNotifier(""); @@ -24,10 +27,6 @@ class OtpDialog { Future? timer; - static BuildContext? _context; - - static bool? _loading; - OtpDialog(this.context, this.type, this.mobileNo, this.onSuccess, this.onFailure, {required this.onResendCode}); GlobalKey? verifyAccountForm = GlobalKey(); @@ -39,12 +38,7 @@ class OtpDialog { TextEditingController digit3 = TextEditingController(text: ""); TextEditingController digit4 = TextEditingController(text: ""); - Map verifyAccountFormValue = { - 'digit1': '', - 'digit2': '', - 'digit3': '', - 'digit4': '', - }; + Map verifyAccountFormValue = {'digit1': '', 'digit2': '', 'digit3': '', 'digit4': ''}; final focusD1 = FocusNode(); final focusD2 = FocusNode(); final focusD3 = FocusNode(); @@ -53,24 +47,27 @@ class OtpDialog { // ProjectViewModel projectProvider; String displayTime = ''; - String? _code; dynamic setState; bool stopTimer = false; + bool isCodeExpired = false; // static String signature; void displayDialog(BuildContext context) async { - return showDialog( - context: context, - barrierColor: Colors.black.withOpacity(0.63), - builder: (context) { - // projectProvider = Provider.of(context); - return Dialog( - backgroundColor: Colors.white, - surfaceTintColor: Colors.transparent, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), - insetPadding: const EdgeInsets.only(left: 21, right: 21), - child: StatefulBuilder(builder: (context, setState) { + otpFieldClear.value = ""; + _startListeningForSms(); + showDialog( + context: context, + barrierColor: Colors.black.withOpacity(0.63), + builder: (context) { + // projectProvider = Provider.of(context); + return Dialog( + backgroundColor: Colors.white, + surfaceTintColor: Colors.transparent, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + insetPadding: const EdgeInsets.only(left: 21, right: 21), + child: StatefulBuilder( + builder: (context, setState) { if (displayTime == '') { startTimer(setState); } @@ -85,10 +82,10 @@ class OtpDialog { type == 1 ? "assets/images/login/verify_sms.svg" : type == 2 - ? "assets/images/login/verify_whatsapp.svg" - : type == 3 - ? "assets/images/login/verify_face.svg" - : 'assets/images/login/verify_thumb.svg', + ? "assets/images/login/verify_whatsapp.svg" + : type == 3 + ? "assets/images/login/verify_face.svg" + : 'assets/images/login/verify_thumb.svg', height: 50, width: 50, ), @@ -96,7 +93,20 @@ class OtpDialog { LocaleKeys.otp.tr().toText14(), LocaleKeys.verification.tr().toText24(isBold: true), 6.height, - (LocaleKeys.pleaseEnterTheVerificationCodeSentTo.tr() + ' xxxxxxxx' + mobileNo.toString().substring(mobileNo.toString().length - 3)).toText16(), + Builder( + builder: (context) { + String maskedNumber = ""; + if (mobileNo != null) { + String numStr = mobileNo.toString(); + if (numStr.length >= 3) { + maskedNumber = ' xxxxxxxx' + numStr.substring(numStr.length - 3); + } else { + maskedNumber = ' ' + numStr; + } + } + return (LocaleKeys.pleaseEnterTheVerificationCodeSentTo.tr() + maskedNumber).toText16(); + }, + ), 18.height, ValueListenableBuilder( builder: (BuildContext context, String value, Widget? child) { @@ -128,92 +138,63 @@ class OtpDialog { ), ), 10.height, - stopTimer + isCodeExpired ? Row( - children: [ - Expanded( - child: LocaleKeys.codeExpire.tr().toText16( - color: MyColors.redColor, - ), - ), - 12.width, - Image.asset( - "assets/icons/ic_alarm.png", - width: 20, - height: 20, - color: MyColors.redColor, - ), - ], - ) + children: [ + Expanded(child: LocaleKeys.codeExpire.tr().toText16(color: MyColors.redColor)), + 12.width, + Image.asset("assets/icons/ic_alarm.png", width: 20, height: 20, color: MyColors.redColor), + ], + ) : RichText( - text: TextSpan( - text: LocaleKeys.theVerificationCodeWillExpireIn.tr() + '\n', - style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: MyColors.darkTextColor, letterSpacing: -0.48), - children: [ - TextSpan( - text: displayTime, - style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: MyColors.textMixColor, letterSpacing: -0.48), - ), - ], - ), + text: TextSpan( + text: LocaleKeys.theVerificationCodeWillExpireIn.tr() + '\n', + style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: MyColors.darkTextColor, letterSpacing: -0.48), + children: [ + TextSpan(text: displayTime, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: MyColors.textMixColor, letterSpacing: -0.48)), + ], ), + ), ], ); }, valueListenable: otpFieldClear, ), 18.height, - DefaultButton( - stopTimer ? LocaleKeys.resend.tr() : LocaleKeys.cancel.tr(), - () async { - if (stopTimer) { - hideSMSBox(context); - onResendCode(); - } else { - stopTimer = true; - // onFailure(); - hideSMSBox(context); - } - }, - colors: stopTimer - ? null - : [ - MyColors.pinkDarkColor, - MyColors.pinkDarkColor, - ], - ), + DefaultButton(isCodeExpired ? LocaleKeys.resend.tr() : LocaleKeys.cancel.tr(), () async { + if (isCodeExpired) { + hideSMSBox(context); + onResendCode(); + } else { + stopTimer = true; + hideSMSBox(context); + } + }, colors: isCodeExpired ? null : [MyColors.pinkDarkColor, MyColors.pinkDarkColor]), ], ), ); - }), - ); - }); + }, + ), + ); + }, + ).then((_) { + stopTimer = true; + _stopListeningForSms(); + }); } InputDecoration buildInputDecoration(BuildContext context) { return InputDecoration( counterText: " ", - enabledBorder: const OutlineInputBorder( - borderRadius: BorderRadius.all(Radius.circular(10)), - borderSide: BorderSide(color: Colors.black), - ), - focusedBorder: OutlineInputBorder( - borderRadius: const BorderRadius.all(Radius.circular(10.0)), - borderSide: BorderSide(color: Theme.of(context).primaryColor), - ), - errorBorder: OutlineInputBorder( - borderRadius: const BorderRadius.all(Radius.circular(10.0)), - borderSide: BorderSide(color: Colors.red), - ), + enabledBorder: const OutlineInputBorder(borderRadius: BorderRadius.all(Radius.circular(10)), borderSide: BorderSide(color: Colors.black)), + focusedBorder: OutlineInputBorder(borderRadius: const BorderRadius.all(Radius.circular(10.0)), borderSide: BorderSide(color: Theme.of(context).primaryColor)), + errorBorder: OutlineInputBorder(borderRadius: const BorderRadius.all(Radius.circular(10.0)), borderSide: BorderSide(color: Colors.red)), // focusedErrorBorder: OutlineInputBorder( // borderRadius: const BorderRadius.all(Radius.circular(10.0)), // borderSide: BorderSide(color: Colors.red), // // borderSide: BorderSide(color: Theme.of(context).colorScheme.error), // ), - focusedErrorBorder: OutlineInputBorder( - borderRadius: const BorderRadius.all(Radius.circular(10.0)), - borderSide: BorderSide(color: Theme.of(context).colorScheme.error), - ), + focusedErrorBorder: OutlineInputBorder(borderRadius: const BorderRadius.all(Radius.circular(10.0)), borderSide: BorderSide(color: Theme.of(context).colorScheme.error)), ); } @@ -240,40 +221,120 @@ class OtpDialog { } void startTimer(setState) { + if (stopTimer) return; remainingTime--; if (stopTimer) return; - setState(() { - displayTime = getSecondsAsDigitalClock(remainingTime); - }); + try { + setState(() { + displayTime = getSecondsAsDigitalClock(remainingTime); + }); + } catch (_) {} timer = Future.delayed(const Duration(seconds: 1), () { + if (stopTimer) return; if (remainingTime > 0) { startTimer(setState); } else { - setState(() { - stopTimer = true; - }); + if (stopTimer) return; + try { + setState(() { + isCodeExpired = true; + stopTimer = true; + }); + } catch (_) {} + _stopListeningForSms(); } }); } void hideSMSBox(context) { + stopTimer = true; + _stopListeningForSms(); onFailure(); } void _onOtpCallBack(String otpCode, bool? isAutofill) { + if (isCodeExpired) return; if (otpCode.length == 4) { - // stopTimer = true; + _stopListeningForSms(); otpFieldClear.value = otpCode; onSuccess(otpCode, _pinPutController); } } -// static getSignature() async { -// // if (Platform.isAndroid) { -// // return await SmsRetriever.getAppSignature(); -// // } else { -// // return null; -// // } -// } + void _startListeningForSms() { + if (!Platform.isAndroid) return; + try { + SmsVerification.startListeningSms() + .then((String? message) { + if (stopTimer || isCodeExpired) return; + if (message != null && message.isNotEmpty) { + debugPrint("SMS OTP Auto-Verify received message: $message"); + String? otpCode = extractOtpCode(message); + if (otpCode != null && otpCode.length == 4) { + debugPrint("Auto-filling OTP: $otpCode"); + _pinPutController.text = otpCode; + _pinPutController.selection = TextSelection.collapsed(offset: otpCode.length); + _onOtpCallBack(otpCode, true); + } + } + }) + .catchError((e) { + debugPrint("Error in startListeningSms: $e"); + }); + } catch (e) { + debugPrint("Error starting SMS listening: $e"); + } + } + + void _stopListeningForSms() { + if (!Platform.isAndroid) return; + try { + SmsVerification.stopListening(); + } catch (e) { + debugPrint("Error stopping SMS listening: $e"); + } + } + + static String? extractOtpCode(String? message) { + if (message == null || message.trim().isEmpty) return null; + String normalized = _normalizeDigits(message); + + // 1. Try matching with OTP context keyword if available + RegExp contextRegex = RegExp(r'(?:code|otp|رمز|كود|تفعيل|تحقق)[^\d]{0,15}(\d{4})\b', caseSensitive: false); + Match? contextMatch = contextRegex.firstMatch(normalized); + if (contextMatch != null && contextMatch.group(1) != null) { + return contextMatch.group(1); + } + + // 2. Try exact 4-digit word boundary match + Iterable matches = RegExp(r'\b\d{4}\b').allMatches(normalized); + if (matches.isNotEmpty) { + return matches.first.group(0); + } + + // 3. Fallback + try { + Iterable allDigits = RegExp(r'\d{4}').allMatches(normalized); + if (allDigits.isNotEmpty) { + return allDigits.first.group(0); + } + } catch (_) {} + + return null; + } + + static String _normalizeDigits(String input) { + const String arabic = '٠١٢٣٤٥٦٧٨٩'; + const String english = '0123456789'; + String res = input; + for (int i = 0; i < arabic.length; i++) { + res = res.replaceAll(arabic[i], english[i]); + } + return res; + } + + static Future getSignature() async { + return await LoginApiClient.getSignature(); + } } diff --git a/lib/extensions/string_extensions.dart b/lib/extensions/string_extensions.dart index 93a6cf8..14dca8f 100644 --- a/lib/extensions/string_extensions.dart +++ b/lib/extensions/string_extensions.dart @@ -63,63 +63,68 @@ extension EmailValidator on String { Widget get toWidget => Text(this); Widget toText10({Color? color, bool isBold = false, int? maxlines, FontStyle? fontStyle}) => Text( - this, - maxLines: maxlines, - style: TextStyle(fontSize: 10, fontStyle: fontStyle ?? FontStyle.normal, fontWeight: isBold ? FontWeight.bold : FontWeight.w600, color: color ?? MyColors.darkTextColor, letterSpacing: -0.4), - ); - - Widget toText11({Color? color, FontWeight? weight, bool isUnderLine = false, bool isBold = false, bool isCenter = false, int maxLine = 0}) => Text( - this, - textAlign: isCenter ? TextAlign.center : null, - maxLines: (maxLine > 0) ? maxLine : null, - style: TextStyle( - fontSize: 11, - fontWeight: weight ?? (isBold ? FontWeight.bold : FontWeight.w600), - color: color ?? MyColors.darkTextColor, - letterSpacing: -0.33, - decoration: isUnderLine ? TextDecoration.underline : null, - ), - ); - - Widget toText12({Color? color, bool isUnderLine = false, bool isBold = false, bool isCenter = false, int? maxLine, TextOverflow? overflow}) => Text( - this, - textAlign: isCenter ? TextAlign.center : null, - maxLines: maxLine == 0 ? null : maxLine, - style: TextStyle( - fontSize: 12, - fontWeight: isBold ? FontWeight.bold : FontWeight.w600, - color: color ?? MyColors.darkTextColor, - letterSpacing: -0.72, - decoration: isUnderLine ? TextDecoration.underline : null, - overflow: TextOverflow.visible - ), - ); - - Widget toText12Auto({Color? color, bool isUnderLine = false, bool isBold = false, bool isCenter = false, int? maxLine,TextOverflow? overflow}) => AutoSizeText( - this, - textAlign: isCenter ? TextAlign.center : null, - maxLines: maxLine == 0 ? null : maxLine, - minFontSize: 8, - style: TextStyle( - fontSize: 12, - fontWeight: isBold ? FontWeight.bold : FontWeight.w600, - color: color ?? MyColors.darkTextColor, - overflow: overflow, - letterSpacing: -0.72, - decoration: isUnderLine ? TextDecoration.underline : null, - ), - ); - - Widget toTextAuto({ + this, + maxLines: maxlines, + style: TextStyle(fontSize: 10, fontStyle: fontStyle ?? FontStyle.normal, fontWeight: isBold ? FontWeight.bold : FontWeight.w600, color: color ?? MyColors.darkTextColor, letterSpacing: -0.4), + ); + + Widget toText11({Color? color, FontWeight? weight, bool isUnderLine = false, bool isBold = false, bool isCenter = false, int maxLine = 0, TextOverflow? overflow}) => Text( + this, + textAlign: isCenter ? TextAlign.center : null, + maxLines: (maxLine > 0) ? maxLine : null, + overflow: overflow, + style: TextStyle( + fontSize: 11, + fontWeight: weight ?? (isBold ? FontWeight.bold : FontWeight.w600), + color: color ?? MyColors.darkTextColor, + letterSpacing: -0.33, + decoration: isUnderLine ? TextDecoration.underline : null, + ), + ); + + Widget toText12({ Color? color, + FontWeight? weight, bool isUnderLine = false, + Color? underlineColor, + double underlineGap = 2, bool isBold = false, bool isCenter = false, - int maxLine = 0, - double fontSize = 12, - double letterSpacing = -0.72, - double height = 1, - }) => + int? maxLine, + TextOverflow? overflow, + }) { + Widget text = Text( + this, + textAlign: isCenter ? TextAlign.center : null, + maxLines: maxLine == 0 ? null : maxLine, + style: TextStyle(fontSize: 12, fontWeight: weight ?? (isBold ? FontWeight.bold : FontWeight.w600), color: color ?? MyColors.darkTextColor, letterSpacing: -0.72, overflow: TextOverflow.visible), + ); + + if (!isUnderLine) return text; + + return Container( + padding: EdgeInsets.only(bottom: underlineGap), + decoration: BoxDecoration(border: Border(bottom: BorderSide(color: underlineColor ?? color ?? MyColors.darkTextColor, width: 1))), + child: text, + ); + } + + Widget toText12Auto({Color? color, bool isUnderLine = false, bool isBold = false, bool isCenter = false, int? maxLine, TextOverflow? overflow}) => AutoSizeText( + this, + textAlign: isCenter ? TextAlign.center : null, + maxLines: maxLine == 0 ? null : maxLine, + minFontSize: 8, + style: TextStyle( + fontSize: 12, + fontWeight: isBold ? FontWeight.bold : FontWeight.w600, + color: color ?? MyColors.darkTextColor, + overflow: overflow, + letterSpacing: -0.72, + decoration: isUnderLine ? TextDecoration.underline : null, + ), + ); + + Widget toTextAuto({Color? color, bool isUnderLine = false, bool isBold = false, bool isCenter = false, int maxLine = 0, double fontSize = 12, double letterSpacing = -0.72, double height = 1}) => AutoSizeText( this, textAlign: isCenter ? TextAlign.center : null, @@ -134,100 +139,103 @@ extension EmailValidator on String { ), ); - Widget toText13({Color? color, bool isUnderLine = false}) => Text( - this, - style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: color ?? MyColors.darkTextColor, letterSpacing: -0.52, decoration: isUnderLine ? TextDecoration.underline : null), - ); - - Widget toText14({Color? color, bool isUnderLine = false, bool isBold = false, FontWeight? weight, int? maxlines, TextAlign? textAlign, bool isCenter = false}) => Text( - this, - textAlign: isCenter ? TextAlign.center : (textAlign ?? TextAlign.left), - maxLines: maxlines, - style: TextStyle( - color: color ?? MyColors.darkTextColor, - fontSize: 14, - letterSpacing: -0.48, - fontWeight: weight ?? (isBold ? FontWeight.bold : FontWeight.w600), - decoration: isUnderLine ? TextDecoration.underline : null), - ); - - Widget toText16({Color? color, bool isUnderLine = false, bool isBold = false, int? maxlines, double? height, bool isCentered = false}) => Text( - this, - maxLines: maxlines, - textAlign: isCentered ? TextAlign.center : null, - style: TextStyle( - color: color ?? MyColors.darkTextColor, - fontSize: 16, - letterSpacing: -0.64, - height: height, - fontWeight: isBold ? FontWeight.bold : FontWeight.w600, - decoration: isUnderLine ? TextDecoration.underline : null, - ), - ); - - Widget toText17({Color? color, bool isBold = false, bool isCentered = false}) => Text( - this, - textAlign: isCentered ? TextAlign.center : null, - style: TextStyle(color: color ?? MyColors.darkTextColor, fontSize: 17, letterSpacing: -0.68, fontWeight: isBold ? FontWeight.bold : FontWeight.w600), - ); - - Widget toText18({Color? color, bool isBold = false, bool isCentered = false}) => Text( - this, - textAlign: isCentered ? TextAlign.center : null, - style: TextStyle(fontSize: 18, fontWeight: isBold ? FontWeight.bold : FontWeight.w600, color: color ?? MyColors.darkTextColor, letterSpacing: -1.08), - ); - - Widget toText19({Color? color, bool isBold = false}) => Text( - this, - style: TextStyle(fontSize: 19, fontWeight: isBold ? FontWeight.bold : FontWeight.w600, color: color ?? MyColors.darkTextColor, letterSpacing: -1.14), - ); - - Widget toText20({Color? color, bool isBold = false, bool isCentered = false}) => Text( - this, - textAlign: isCentered ? TextAlign.center : null, - style: TextStyle(fontSize: 20, fontWeight: isBold ? FontWeight.bold : FontWeight.w600, color: color ?? MyColors.darkTextColor, letterSpacing: -0.4), - ); - - Widget toText21({Color? color, bool isBold = false, FontWeight? weight, int? maxlines}) => Text( - this, - maxLines: maxlines, - style: TextStyle(color: color ?? MyColors.grey3AColor, fontSize: 21, letterSpacing: -0.84, fontWeight: weight ?? (isBold ? FontWeight.bold : FontWeight.w600)), - ); + Widget toText13({Color? color, FontWeight? weight, bool isUnderLine = false, bool isBold = false, int? maxlines, TextOverflow? overflow}) => Text( + this, + maxLines: maxlines, + overflow: overflow, + style: TextStyle( + fontSize: 13, + fontWeight: weight ?? (isBold ? FontWeight.bold : FontWeight.w600), + color: color ?? MyColors.darkTextColor, + letterSpacing: -0.52, + decoration: isUnderLine ? TextDecoration.underline : null, + ), + ); + + Widget toText14({Color? color, bool isUnderLine = false, bool isBold = false, FontWeight? weight, int? maxlines, TextAlign? textAlign, bool isCenter = false, TextOverflow? overflow}) => Text( + this, + textAlign: isCenter ? TextAlign.center : textAlign, + maxLines: maxlines, + style: TextStyle( + overflow: overflow, + color: color ?? MyColors.darkTextColor, + fontSize: 14, + letterSpacing: -0.48, + fontWeight: weight ?? (isBold ? FontWeight.bold : FontWeight.w600), + decoration: isUnderLine ? TextDecoration.underline : null, + ), + ); + + Widget toText16({Color? color, FontWeight? weight, bool isUnderLine = false, bool isBold = false, int? maxlines, double? height, bool isCentered = false}) => Text( + this, + maxLines: maxlines, + textAlign: isCentered ? TextAlign.center : null, + style: TextStyle( + color: color ?? MyColors.darkTextColor, + fontSize: 16, + letterSpacing: -0.64, + height: height, + fontWeight: weight ?? (isBold ? FontWeight.bold : FontWeight.w600), + decoration: isUnderLine ? TextDecoration.underline : null, + ), + ); + + Widget toText17({Color? color, FontWeight? weight, bool isBold = false, bool isCentered = false}) => Text( + this, + textAlign: isCentered ? TextAlign.center : null, + style: TextStyle(color: color ?? MyColors.darkTextColor, fontSize: 17, letterSpacing: -0.68, fontWeight: weight ?? (isBold ? FontWeight.bold : FontWeight.w600)), + ); + + Widget toText18({Color? color, FontWeight? weight, bool isBold = false, bool isCentered = false}) => Text( + this, + textAlign: isCentered ? TextAlign.center : null, + style: TextStyle(fontSize: 18, fontWeight: weight ?? (isBold ? FontWeight.bold : FontWeight.w600), color: color ?? MyColors.darkTextColor, letterSpacing: -1.08), + ); + + Widget toText19({Color? color, bool isBold = false}) => + Text(this, style: TextStyle(fontSize: 19, fontWeight: isBold ? FontWeight.bold : FontWeight.w600, color: color ?? MyColors.darkTextColor, letterSpacing: -1.14)); + + Widget toText20({Color? color, FontWeight? weight, bool isBold = false, bool isCentered = false}) => Text( + this, + textAlign: isCentered ? TextAlign.center : null, + style: TextStyle(fontSize: 20, fontWeight: weight ?? (isBold ? FontWeight.bold : FontWeight.w600), color: color ?? MyColors.darkTextColor, letterSpacing: -0.4), + ); + + Widget toText21({Color? color, bool isBold = false, FontWeight? weight, int? maxlines}) => + Text(this, maxLines: maxlines, style: TextStyle(color: color ?? MyColors.grey3AColor, fontSize: 21, letterSpacing: -0.84, fontWeight: weight ?? (isBold ? FontWeight.bold : FontWeight.w600))); Widget toText22({Color? color, bool isBold = false, bool isCentered = false}) => Text( - this, - textAlign: isCentered ? TextAlign.center : null, - style: TextStyle(height: 1, color: color ?? MyColors.darkTextColor, fontSize: 22, letterSpacing: -1.44, fontWeight: isBold ? FontWeight.bold : FontWeight.w600), - ); + this, + textAlign: isCentered ? TextAlign.center : null, + style: TextStyle(height: 1, color: color ?? MyColors.darkTextColor, fontSize: 22, letterSpacing: -1.44, fontWeight: isBold ? FontWeight.bold : FontWeight.w600), + ); Widget toText23({Color? color, bool isBold = false, bool isCentered = false}) => Text( - this, - textAlign: isCentered ? TextAlign.center : null, - style: TextStyle(height: 23 / 24, color: color ?? MyColors.darkTextColor, fontSize: 23, letterSpacing: -0.92, fontWeight: isBold ? FontWeight.bold : FontWeight.w600), - ); + this, + textAlign: isCentered ? TextAlign.center : null, + style: TextStyle(height: 23 / 24, color: color ?? MyColors.darkTextColor, fontSize: 23, letterSpacing: -0.92, fontWeight: isBold ? FontWeight.bold : FontWeight.w600), + ); Widget toText24({Color? color, bool isBold = false, bool isCentered = false}) => Text( - this, - textAlign: isCentered ? TextAlign.center : null, - style: TextStyle(height: 23 / 24, color: color ?? MyColors.darkTextColor, fontSize: 24, letterSpacing: -1.44, fontWeight: isBold ? FontWeight.bold : FontWeight.w600), - ); + this, + textAlign: isCentered ? TextAlign.center : null, + style: TextStyle(height: 23 / 24, color: color ?? MyColors.darkTextColor, fontSize: 24, letterSpacing: -1.44, fontWeight: isBold ? FontWeight.bold : FontWeight.w600), + ); Widget toText30({Color? color, bool isBold = false, bool isCentered = false}) => Text( - this, - textAlign: isCentered ? TextAlign.center : null, - style: TextStyle(height: 20 / 32, color: color ?? MyColors.darkTextColor, fontSize: 32, letterSpacing: -1.2, fontWeight: isBold ? FontWeight.bold : FontWeight.w600), - ); + this, + textAlign: isCentered ? TextAlign.center : null, + style: TextStyle(height: 20 / 32, color: color ?? MyColors.darkTextColor, fontSize: 32, letterSpacing: -1.2, fontWeight: isBold ? FontWeight.bold : FontWeight.w600), + ); Widget toText32({Color? color, bool isBold = false, bool isCentered = false}) => Text( - this, - textAlign: isCentered ? TextAlign.center : null, - style: TextStyle(height: 32 / 32, color: color ?? MyColors.darkTextColor, fontSize: 32, letterSpacing: -1.92, fontWeight: isBold ? FontWeight.bold : FontWeight.w600), - ); + this, + textAlign: isCentered ? TextAlign.center : null, + style: TextStyle(height: 32 / 32, color: color ?? MyColors.darkTextColor, fontSize: 32, letterSpacing: -1.92, fontWeight: isBold ? FontWeight.bold : FontWeight.w600), + ); - Widget toText44({Color? color, bool isBold = false}) => Text( - this, - style: TextStyle(height: 32 / 32, color: color ?? MyColors.darkTextColor, fontSize: 44, letterSpacing: -2.64, fontWeight: isBold ? FontWeight.bold : FontWeight.w600), - ); + Widget toText44({Color? color, bool isBold = false}) => + Text(this, style: TextStyle(height: 32 / 32, color: color ?? MyColors.darkTextColor, fontSize: 44, letterSpacing: -2.64, fontWeight: isBold ? FontWeight.bold : FontWeight.w600)); Widget toSectionHeading({String upperHeading = "", String lowerHeading = ""}) { String upper = ""; @@ -251,14 +259,7 @@ extension EmailValidator on String { lower = lowerHeading; } - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - if (upper.isNotEmpty) upper.toText12(), - lower.toText24(isBold: true), - ], - ); + return Column(crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [if (upper.isNotEmpty) upper.toText12(), lower.toText24(isBold: true)]); } bool isValidEmail() { diff --git a/lib/generated/codegen_loader.g.dart b/lib/generated/codegen_loader.g.dart index 4c96539..669b159 100644 --- a/lib/generated/codegen_loader.g.dart +++ b/lib/generated/codegen_loader.g.dart @@ -6,7 +6,7 @@ import 'dart:ui'; import 'package:easy_localization/easy_localization.dart' show AssetLoader; -class CodegenLoader extends AssetLoader{ +class CodegenLoader extends AssetLoader { const CodegenLoader(); @override @@ -14,1379 +14,1863 @@ class CodegenLoader extends AssetLoader{ return Future.value(mapLocales[locale.toString()]); } - static const Map _ar_SA = { - "mohemm": "Mohemm", - "english": "English", - "arabic": "عربي", - "login": "تسجيل الدخول", - "pleaseEnterLoginDetails": "الرجاء إدخال التفاصيل أدناه لتسجيل الدخول", - "userId": "UserID", - "username": "اسم المستخدم", - "userName": "اسم المستخدم", - "userSearch": "User Search", - "password": "كلمة المرور", - "welcomeBack": "مرحبا بعودتك", - "wouldYouLikeToLoginWithCurrentUsername": "هل ترغب في تسجيل الدخول باسم المستخدم الحالي؟", - "lastLoginDetails": "تفاصيل تسجيل الدخول الأخير:", - "verificationType": "نوع التحقق:", - "pleaseVerify": "ارجوك تحقق", - "pleaseVerifyForBio": "الرجاء التحقق من تسجيل الدخول باستخدام أحد هذه الخيارات", - "verifyThroughFace": "تحقق من خلال الوجه", - "verifyThroughFingerprint": "تحقق من خلال بصمة الإصبع", - "verifyThroughSMS": "تحقق من خلال الرسائل القصيرة", - "verifyThroughWhatsapp": "تحقق من خلال Whatsapp", - "useAnotherAccount": "استخدم حسابا آخر", - "pleaseEnterTheVerificationCodeSentTo": "الرجاء إدخال رمز التحقق المرسل إلى ", - "theVerificationCodeWillExpireIn": "ستنتهي صلاحية رمز التحقق في ", - "goodMorning": "صباح الخير", - "markAttendance": "علامة الحضور", - "timeLeftToday": "الوقت المتبقي اليوم", - "checkIn": "تحقق في", - "workList": "قائمة العمل", - "leaveBalance": "رصيد الاجازات", - "missingSwipes": "تسجيل بصمة حضور", - "ticketBalance": "رصيد التذكرة", - "other": "آخر", - "services": "خدمات", - "viewAllServices": "عرض جميع الخدمات", - "monthlyAttendance": "الحضور الشهري", - "vacationRule": "قاعدة الاجازات", - "vacationType": "نوع الاجازة", - "startDateT": "تاريخ البدء", - "endDateT": "تاريخ الانتهاء", - "workFromHome": "العمل من المنزل", - "ticketRequest": "طلب تذكرة", - "viewAllOffers": "مشاهدة جميع العروض", - "offers": "عروض & ", - "discounts": "الخصومات", - "newString": "جديد", - "setTheNewPassword": "قم بتعيين كلمة المرور الجديدة", - "typeYourNewPasswordBelow": "اكتب كلمة المرور الجديدة أدناه", - "typeYourNewActiveDirectoryPasswordBelow": "اكتب كلمة مرور الدليل النشط الجديدة أدناه", - "confirmPassword": "تأكيد كلمة المرور", - "update": "تحديث", - "title": "عنوان", - "home": "الرئيسية", - "mySalary": "راتبي", - "createRequest": "إنشاء طلب", - "forgotPassword": "هل نسيت كلمة السر", - "employeeId": "هوية الموظف", - "loginCodeWillSentToMobileNumber": "الرجاء إدخال معرف الموظف الخاص بك ، وسيتم إرسال رمز تسجيل الدخول إلى رقم هاتفك المحمول", - "changePassword": "تغيير كلمة المرور", - "ok": "موافق", - "confirm": "تؤكد", - "passwordChangedSuccessfully": "تم تغيير الرقم السري بنجاح", - "itemsForSale": "سلع للبيع", - "attendanceDetails": "تفاصيل الحضور", - "order": "الطلبات", - "earlyOut": "الخروج مبكرا", - "shortage": "ساعات التقصير", - "excess": "فائض", - "lateIn": "القدوم المتاخر", - "approvedCheckOut": "اعتماد وقت الخروج", - "approvedCheckIn": "اعتماد وقت الدخول", - "actualCheckOut": "وقت الخروج", - "actualCheckIn": "وقت الدخول", - "present": "حضور", - "pres": "حضور", - "shiftTime": "وقت التناوب", - "absent": "غياب", - "attendance": "الحضور", - "scheduleDays": "ايام العمل", - "offDays": "ايام الراحه", - "nonAnalyzed": "لايوجد تحليل", - "shortageHour": "ساعات التقصير", - "stats": "الحاله", - "completed": "تم اكمال", - "msg": "Hello {} in the {} world ", - "msg_named": "{} are written in the {lang} language", - "clickMe": "Click me", - "doNotUseRecentPassword": "لا تستخدم كلمة مرور حديثة", - "atLeastOneLowercase": "حرف صغير واحد على الأقل", - "atLeastOneUppercase": "حرف كبير واحد على الأقل", - "atLeastOneNumeric": "رقم واحد على الأقل", - "minimum8Characters": "8 أحرف على الأقل", - "doNotAddRepeatingLetters": "لا تقم بإضافة أحرف متكررة", - "itShouldContainSpecialCharacter": "يجب لا يحتوي على أحرف خاصة", - "confirmPasswordMustMatch": "يجب أن يتطابق تأكيد كلمة المرور", - "sms": "رسالة قصيرة", - "fingerPrint": "بصمة", - "face": "التعرف على الوجه", - "whatsapp": "واتس اب", - "reject": "يرفض", - "approve": "يوافق", - "cancel": "إلغاء", - "generate": "يولد", - "requestedItems": "العناصر المطلوبة", - "request": "طلب", - "myRequest": "طلبي", - "actions": "أجراءات", - "delegate": "مندوب", - "request_info": "اطلب معلومات", - "attachments": "المرفقات", - "info": "معلومات", - "employeeNumber": "رقم الموظف", - "assignmentNumber": "رقم الواجب", - "employeeName": "اسم الموظف", - "scheduleDate": "تاريخ الجدول الزمني", - "shiftType": "نوع التحول", - "shift": "يحول", - "breakText": "استراحة", - "actualSwipeStart": "بدء التمرير الفعلي", - "actualSwipeEnd": "التمرير الفعلي للنهاية", - "approvedSwipeStart": "وافق انتقاد البدء", - "approvedSwipeStartReason": "تمت الموافقة على سبب بدء التمرير السريع", - "approvedSwipeEnd": "تمت الموافقة على تمرير النهاية", - "approvedSwipeEndReason": "الموافقة على سبب إنهاء التمرير", - "from": "من", - "to": "ل", - "sent": "أرسلت", - "closed": "مغلق", - "id": "هوية شخصية", - "responder": "المستجيب", - "jobTitle": "عنوان وظيفي", - "grade": "درجة", - "jobCategory": "تصنيف الوظيفة", - "category": "فئة", - "employeeEmailAddress": "عنوان البريد الإلكتروني للموظف", - "payrollBranch": "فرع الرواتب", - "yourChangeHasBeenSavedSuccessfully": "تم حفظ التغيير الخاص بك بنجاح", - "code": "شفرة", - "unit": "وحدة", - "quantity": "كمية", - "dateRequired": "التاريخ مطلوب", - "lineStatus": "حالة الخط", - "statusDate": "تاريخ الحالة", - "transactionType": "نوع المعاملة", - "operatingUnit": "وحدة التشغيل", - "organizationCode": "كود المنظمة", - "organization": "منظمة", - "fromSubInventory": "من الجرد الفرعي", - "fromLocator": "من محدد المواقع", - "toSubInventory": "إلى الجرد الفرعي", - "toLocator": "إلى محدد المواقع", - "shipToLocator": "شحن إلى محدد المواقع", - "itemHistory": "تاريخ العنصر", - "mfg": "مبدع", - "lineType": "نوع الخط", - "price": "السعر", - "lineAmount": "مبلغ الخط", - "lineDiscount": "خصم الخط٪", - "needByDate": "القادمة إلى الأمام", - "promisedDate": "التسجيل وعد", - "deliverToLocation": "تسليم إلى الموقع", - "requisitionNumber": "رقم الطلب", - "requester": "مقدم الطلب", - "quotationAnalysis": "تحليل الاقتباس", - "subject": "موضوعات", - "description": "وصف", - "pleaseEnterDescription": "الرجاء إدخال الوصف", - "supplier": "المورد", - "site": "موقع", - "buyer": "مشتر", - "preparer": "معد", - "creationDate": "تاريخ الإنشاء", - "shipToLocation": "الشحن الى الموقع", - "quotationNumber": "رقم الإقتباس", - "quotationDate": "تاريخ الاقتباس", - "paymentTerms": "شروط الدفع", - "currency": "عملة", - "grossAmount": "المبلغ الإجمالي", - "discountAmount": "مقدار الخصم", - "customDuty": "الرسوم الجمركية", - "shipHandle": "مقبض السفينة", - "otherCharges": "رسوم أخرى", - "totalPOAmountWithVAT": "إجمالي مبلغ الشراء مع ضريبة القيمة المضافة", - "totalPOAmountInWords": "إجمالي مبلغ أمر الشراء بالكلمات", - "requestNumber": "رقم الطلب", - "uom": "UOM", - "operatingCode": "كود التشغيل", - "poNumber": "PO عدد", - "revision": "مراجعة", - "quantityOrdered": "الكمية المطلوبة", - "quantityReceived": "الكمية المستلمة", - "bonusQuantity": "كمية المكافأة", - "purchasePrice": "سعر الشراء", - "discountPer": "خصم ٪", - "balanceQuantity": "كمية التوازن", - "netPrice": "السعر الصافي", - "closureStatus": "حالة الإغلاق", - "quotationNetPrice": "صافي سعر الاقتباس", - "quotationUOM": "اقتباس UOM", - "quotationQty": "اقتباس الكمية", - "itemCode": "رمز الصنف", - "vendorName": "اسم البائع", - "quotationMFGPartNumber": "رقم الجزء MFG الاقتباس", - "quotationDeliveryDate": "تاريخ تسليم عرض الأسعار", - "quotationBonusQuantity": "كمية مكافأة الاقتباس", - "quotationLineTotal": "مجموع خط الاقتباس", - "rfqUOM": "RFQ UOM", - "rfqQty": "RFQ الكمية", - "rfqNumber": "رقم RFQ", - "human": "بشري", - "resources": "موارد", - "details": "تفاصيل", - "noDataAvailable": "لا تتوافر بيانات", - "productName": "اسم المنتج", - "productDescription": "وصف المنتج", - "unitPrice": "سعر الوحده", - "manufacturerName": "اسم المصنع", - "manufacturerPartName": "اسم جزء الشركة المصنعة", - "supplierName": "اسم المورد", - "supplierContact": "الاتصال بالمورد", - "chargeToPatient": "المسؤول عن المريض", - "justification": "التبرير", - "itemDescription": "وصف السلعة", - "groupCode": "كود المجموعة", - "primaryUOM": "UOM الابتدائية", - "subgroupDescription": "وصف المجموعة الفرعية", - "subgroupCode": "رمز المجموعة الفرعية", - "groupDescription": "وصف المجموعة", - "templateName": "اسم القالب", - "itemCreationStatus": "حالة إنشاء العنصر", - "standardizationApprovalStatus": "حالة الموافقة على التقييس", - "standardizationApprovalRejectionReason": "سبب رفض الموافقة على التقييس", - "analyzedBy": "تحليل بواسطة", - "approvedDate": "تاريخ الموافقة", - "itemType": "نوع العنصر", - "relatedTo": "متعلق ب", - "requestDate": "تاريخ الطلب", - "analyzedDate": "تاريخ التحليل", - "urgent": "العاجلة", - "requestDetails": "طلب تفاصيل", - "approvalLevel": "مستوى الموافقة", - "requesterDetails": "تفاصيل مقدم الطلب", - "myAttendance": "حضوري", - "workOnBreak": "التعويض عن العمل اثناءالاستراحه", - "next": "التالي", - "apply": "يتقدم", - "mobile": "التليفون المحمول", - "completingYear": "نحن نقدر لك لاستكمال خدمة", - "year": "سنة", - "month": "شهر", - "day": "يوم", - "address": "العنوان", - "phoneNumber": "رقم الجوال", - "businessGroup": "مجموعة العمل", - "Payroll": "الراتب", - "civilIdentityNumber": "رقم الهويه", - "dateOfBirth": "تاريخ الميلاد", - "maritalStatus ": "الحالة الاجتماعية", - "fullName": "الأسم الكامل", - "remove": "حذف", - "submit": "ارسال", - "submitAsAnonymous": "إرسال كمجهول", - "areYouSureYouWantToSubmit": "هل أنت متأكد أنك تريد أن تقدم؟", - "comments": "تعليقات", - "writeComment": "أكتب تعليقا", - "approversList": "قائمة الموافقين", - "yourRequestHasBeenSubmittedForApprovals": "تم تقديم طلبك للموافقات", - "monthlyPaySlip": "قسيمة الراتب الشهرية", - "particular": "خاص", - "earnings": "أرباح", - "deductions": "الخصومات", - "paymentMethodName": "اسم طريقة الدفع", - "bankName": "اسم البنك", - "branchCode": "رمز الفرع", - "accountNo": "رقم الحساب", - "summaryOfInformation": "ملخص المعلومات", - "totalPayAmount": "المبلغ الإجمالي للدفع", - "paymentInformation": "معلومات الدفع", - "performance": "تقييم الاداء", - "performanceEvaluation": "تقييم الأداء في", - "performanceEvaluationIn": "تقييم أدائك في", - "valuationIn": "تقييم الأداء في", - "amount": "مقدار", - "correctCurrentDatails": "تعديل او اكمال التفاصيل الحالية", - "selectType": " حدد نوع التغيير الذي تريد القيام به", - "enterNewInfo": " أدخل معلومات جديدة بسبب تغيير حقيقي في التفاصيل الحالية (على سبيل المثال بسبب تغيير في الحالة الاجتماعية", - "endDate": "تاريخ الانتهاء", - "removeThisMember": "هل انت متأكد تريد ازالة هذا العضو؟", - "wantUpdateThisMember ": "هل انت متأكد تريد تحديث بيانات هذا العضو؟", - "addNewFamilyMember": "اضافة عضو جديد", - "addRow": "اضافة صف جديد", - "pleaseSelect": "الرجاء اختيار", - "delete": "حذف", - "edit": "تعديل", - "add": "اضافه", - "myProfile": "معلوماتي", - "mowadhafhi": "موظفي", - "searchAnnouncements": "بحث الاعلانات", - "announcements": "اعلانات", - "swipeRequest": "طلب تسجيل حضور", - "serviceType": "نوع الخدمه", - "departmentName": "اسم القسم", - "selectDepartment": "اختر القسم", - "relatedSection": "قسم ذو صله", - "selectSection": "اختيار القسم", - "relatedTopic": "عنوان ذو صله", - "selectTopic": "اختر العنوان", - "supportingDocument": "ارفاق مستند", - "mowadhafhiRequest": "طلب موظفي", - "ticketReference": "مرجع التذكره", - "section": "القسم", - "topic": "العنوان", - "actionBy": "الرد بواسطة", - "pending": "معلق", - "pendingTransactions": "المعاملات المعلقه", - "selectRequestType": "الرجاء اختيار نوع الطلب", - "dateFrom": "من تاريخ", - "dateTo": "الى تاريخ", - "requestName": "اسم الطلب", - "createdFor": "انشاء لأجل", - "requestCreatedSuccessfully": "تم انشاء الطلب بنجاح", - "search": "بحث", - "wantToReject": "هل انت متأكد تريد الرفض", - "requestType": "نوع الطلب", - "employeeDigitalID": "هويةالموظف الرقمية", - "businessCard": "بطاقة العمل", - "viewBusinessCard": "عرض بطاقة العمل", - "logout": "تسجيل خروج", - "checkOut": "وقت الخروج", - "regular": "منتظم", - "mark": "علامة", - "selectMethodOfAttendance": "اختر طريقة تسجيل الحضور", - "comeNearHMGWifi": "HMG wifi من فضلك اقترب من", - "deliverNotificationToMeRegardless": "تسليم الإخطارات إلي بغض النظر عن أي قواعد عامة", - "close": "أغلق", - "respond": "يرد", - "vacationRuleAdded": "تمت إضافة قاعدة الإجازة", - "selectTypeT": "اختر صنف", - "notification": "تنبيه", - "selectNotification": "حدد إعلام", - "ifAllSelectedYouWillSkip": "* إذا تم تحديد الكل ، فستنتقل إلى الخطوة 3", - "applyForVacationRule": "التقدم بطلب للحصول على قانون الإجازة", - "step1": "الخطوة 1", - "step2": "الخطوة 2", - "step3": "الخطوه 3", - "message": "رسالة", - "writeAMessage": "اكتب رسالة", - "notificationReassign": "إعادة تعيين الإخطار", - "selectEmployee": "حدد الموظف", - "searchEmployeeForReplacement": "ابحث عن موظف بديل", - "searchForEmployee": "ابحث عن موظف", - "pleaseSpecifyEndTime": "الرجاء تحديد وقت الانتهاء", - "pleaseSelectNotificationReassign": "يرجى تحديد إعادة تعيين الإخطار", - "pleaseSelectEmployeeForReplacement": "الرجاء تحديد موظف للاستبدال", - "pleaseSelectAction": "الرجاء تحديد الإجراء", - "pleaseSelectDate": "الرجاء تحديد التاريخ", - "todayAttendance": "حضور اليوم", - "viewAttendance": "عرض الحضور", - "teamMembers": "اعضاءالفريق", - "profileDetails": "الملف الشخصي", - "noResultsFound": "لايوجد نتائج", - "searchBy": "بحث بواسطة", - "myTeamMembers": "اعضاء فريقي", - "save": "حفظ", - "TurnNotificationsFor": "تفعيل الاشعارات", - "worklistSettings": "اعدادات الاشعارات", - "absenceType": "نوع الغياب", - "absenceCategory": "فئة الغياب", - "days": "أيام", - "hours": "ساعات", - "approvalStatus": "حالة القبول", - "absenceStatus": "حالة الغياب", - "subordinateLeave": "إجازة التابعيين", - "numberDays": "عدد الأيام", - "poweredBy": "مشغل بواسطة", - "cloudSolutions": "حلول السحابة", - "selectTemplate": "حدد قالب", - "myPostedAds": "إعلاناتي المنشورة", - "browseCategories": "تصفح الفئات", - "searchItems": "عناصر البحث", - "offerAndDiscounts": "العروض والخصومات", - "offerValid": "العرض صالح", - "offerExpired": "انتهى العرض", - "whatAreYouOffering": "ما الذي تعرضه؟", - "selectCategory": "اختر الفئة", - "inProgress": "في تَقَدم", - "locked": "مقفل", - "addDetails": "أضف التفاصيل", - "reviewAndSell": "مراجعة وبيع", - "itemTitle": "عنوان البند", - "itemCondition": "حالة السلعة", - "used": "تستخدم", - "region": "منطقة", - "selectRegion": "اختر المنطقة", - "itemPrice": "سعر السلعة", - "itemPhotos": "صور البند", - "itemInfo": "معلومات العنصر", - "uploadAttachment": "تحميل المرفق", - "selectFromGalleryOrOpenCamera": "اختر من المعرض أو فتح الكاميرا", - "openCamera": "فتح\nالكاميرا", - "uploadFromGallery": "تحميل من\nملفات الجهاز", - "name": "الأسم", - "email": "ايميل", - "noHistoryAvailable": "لايوجد سجل بيانات سابقة ", - "purchaseRequisition": "طلب شراء", - "moveOrder": "طلب تغيير", - "humanResource": "الموارد البشريه", - "purchaseOrder": "امر شراء", - "ITGForms": "ITG نماذج", - "itemCreation": "أنشاء عنصر", - "stamp": "ختم", - "addFavoriteList": "هل تريد اضافة {name} لقائمة المفضله", - "feedbackUserExperience": "هذا للحصول على تعليقات حول تجربة المستخدم", - "rateUI": "كيف تريد أن تقيم", - "rateUI2": "ما مدى رضائك عن هذا التطبيق", - "submitSurvey": "ارسال الاستبيان", - "typeHere": "اكتب هنا", - "infoDetail": "تفاصيل المعلومات", - "amount_detail": "تفاصيل المبلغ", - "currentBalance": "الرصيد الحالي", - "currentLeaveBalance": "رصيد الاجازات الحالي", - "calculatedDays": "الايام المحسوبه", - "totalDays": "مجموع الأيام", - "usedBalance": "المستخدم", - "infants": "رضيع", - "child": "طفل", - "adult": "بالغ", - "updateMember": "هل انت متأكد تريد تحديث بيانات هذا العضو؟", - "fieldIsEmpty": "'{data}' الحقل فارغ. الرجاء التحديد", - "pleaseEnterComments": "الرجاء إدخال التعليقات", - "skip": "يتخطى", - "typeCurrentPasswordBelow": "اكتب كلمة المرور الحاليه", - "currentPassword": "كلمة المرور الحاليه", - "concurrentReports": "التقارير المتزامنه", - "EnterNewAddressMoved": "أدخل عنوان جديد إذا كنت قد انتقلت", - "CorrectAddress": "تصحيح أو تعديل هذا العنوان", - "SelectChangeWantToMake": " حدد نوع التغيير الذي تريد القيام به.", - "profile": { - "reset_password": { - "label": "Reset Password", - "username": "Username", - "password": "password" - }, - "profileCompletionPer": "استكمال الملف الشخصي", - "completeProfile": "الملف الشخصي الكامل", - "personalInformation": "معلومات شخصية", - "basicDetails": "تفاصيل أساسية", + static const Map _ar_SA = { + "mohemm": "MoheM", + "english": "English", + "arabic": "عربي", + "login": "تسجيل الدخول", + "pleaseEnterLoginDetails": "الرجاء إدخال التفاصيل أدناه لتسجيل الدخول", + "userId": "معرّف المستخدم", + "username": "اسم المستخدم", + "userName": "اسم المستخدم", + "userSearch": "البحث عن مستخدم", + "password": "كلمة المرور", + "welcomeBack": "مرحبا بعودتك", + "wouldYouLikeToLoginWithCurrentUsername": + "هل ترغب في تسجيل الدخول باسم المستخدم الحالي؟", + "lastLoginDetails": "تفاصيل تسجيل الدخول الأخير:", + "verificationType": "نوع التحقق:", + "pleaseVerify": "يرجى التحقق", + "pleaseVerifyForBio": + "الرجاء التحقق من تسجيل الدخول باستخدام أحد هذه الخيارات", + "verifyThroughFace": "التحقق بالتعرف على الوجه", + "verifyThroughFingerprint": "التحقق ببصمة الإصبع", + "verifyThroughSMS": "التحقق عبر رسالة نصية", + "verifyThroughWhatsapp": "التحقق عبر واتساب", + "useAnotherAccount": "استخدم حسابا آخر", + "pleaseEnterTheVerificationCodeSentTo": + "الرجاء إدخال رمز التحقق المرسل إلى ", + "theVerificationCodeWillExpireIn": "ستنتهي صلاحية رمز التحقق في ", + "goodMorning": "صباح الخير", + "markAttendance": "تسجيل الحضور", + "timeLeftToday": "الوقت المتبقي اليوم", + "checkIn": "تسجيل الحضور", + "workList": "قائمة العمل", + "leaveBalance": "رصيد الإجازات", + "missingSwipes": "البصمات المفقودة", + "ticketBalance": "رصيد التذاكر", + "other": "آخر", + "services": "خدمات", + "viewAllServices": "عرض جميع الخدمات", + "monthlyAttendance": "الحضور الشهري", + "vacationRule": "قواعد الإجازات", + "vacationType": "نوع الإجازة", + "startDateT": "تاريخ البدء", + "endDateT": "تاريخ الانتهاء", + "workFromHome": "العمل من المنزل", + "ticketRequest": "طلب تذكرة", + "viewAllOffers": "مشاهدة جميع العروض", + "offers": "العروض و", + "discounts": "الخصومات", + "newString": "جديد", + "setTheNewPassword": "قم بتعيين كلمة المرور الجديدة", + "typeYourNewPasswordBelow": "اكتب كلمة المرور الجديدة أدناه", + "typeYourNewActiveDirectoryPasswordBelow": + "اكتب كلمة مرور الدليل النشط الجديدة أدناه", + "confirmPassword": "تأكيد كلمة المرور", + "update": "تحديث", + "title": "عنوان", + "home": "الرئيسية", + "mySalary": "راتبي", + "createRequest": "إنشاء طلب", + "forgotPassword": "هل نسيت كلمة المرور؟", + "employeeId": "الرقم الوظيفي", + "loginCodeWillSentToMobileNumber": + "الرجاء إدخال رقم الموظف الخاص بك ، وسيتم إرسال رمز تسجيل الدخول إلى رقم هاتفك المسجل", + "changePassword": "تغيير كلمة المرور", + "ok": "موافق", + "confirm": "تأكيد", + "passwordChangedSuccessfully": "تم تغيير كلمة المرور بنجاح", + "itemsForSale": "سلع للبيع", + "attendanceDetails": "تفاصيل الحضور", + "order": "الطلبات", + "earlyOut": "الانصراف المبكر", + "shortage": "ساعات غير مغطاة", + "excess": "ساعات العمل الإضافية", + "lateIn": "التأخر عن الحضور", + "approvedCheckOut": "وقت الانصراف المعتمد", + "approvedCheckIn": "وقت الحضور المعتمد", + "actualCheckOut": "وقت الانصراف الفعلي", + "actualCheckIn": "وقت الحضور الفعلي", + "present": "حضور", + "pres": "حضور", + "shiftTime": "وقت المناوبة", + "absent": "غياب", + "attendance": "الحضور", + "scheduleDays": "أيام العمل", + "offDays": "أيام الراحة", + "nonAnalyzed": "لم يتم التحليل", + "shortageHour": "ساعات غير مغطاة", + "stats": "الإحصاءات", + "completed": "مكتمل", + "msg": "مرحبًا {} في عالم {}", + "msg_named": "{} مكتوبة بلغة {lang}", + "clickMe": "اضغط هنا", + "doNotUseRecentPassword": "لا تستخدم كلمة مرور سبق استخدامها مؤخرًا", + "atLeastOneLowercase": "حرف صغير واحد على الأقل", + "atLeastOneUppercase": "حرف كبير واحد على الأقل", + "atLeastOneNumeric": "رقم واحد على الأقل", + "minimum8Characters": "8 أحرف على الأقل", + "doNotAddRepeatingLetters": "لا تقم بإضافة أحرف متكررة", + "itShouldContainSpecialCharacter": "يجب أن تحتوي كلمة المرور على رمز خاص", + "confirmPasswordMustMatch": "يجب أن تتطابق كلمتا المرور", + "sms": "رسالة قصيرة", + "fingerPrint": "بصمة", + "face": "التعرف على الوجه", + "whatsapp": "واتس اب", + "reject": "رفض", + "approve": "موافقة", + "cancel": "إلغاء", + "generate": "إنشاء", + "requestedItems": "العناصر المطلوبة", + "request": "طلب", + "myRequest": "طلباتي", + "actions": "الإجراءات", + "delegate": "تفويض", + "request_info": "طلب معلومات إضافية", + "attachments": "المرفقات", + "info": "معلومات", + "employeeNumber": "رقم الموظف", + "assignmentNumber": "رقم التكليف", + "employeeName": "اسم الموظف", + "scheduleDate": "تاريخ الجدول", + "shiftType": "نوع المناوبة", + "shift": " المناوبة", + "breakText": "استراحة", + "actualSwipeStart": "وقت تسجيل الحضور", + "actualSwipeEnd": "وقت تسجيل المغادرة", + "approvedSwipeStart": "وقت الحضور المعتمد", + "approvedSwipeStartReason": "سبب تعديل وقت الحضور المعتمد", + "approvedSwipeEnd": "وقت الانصراف المعتمد", + "approvedSwipeEndReason": "سبب تعديل وقت الانصراف المعتمد", + "from": "من", + "to": "إلى", + "sent": "تم الإرسال", + "closed": "مغلق", + "id": "المعرّف", + "responder": "المستجيب", + "jobTitle": "المسمى الوظيفي", + "grade": "الدرجة الوظيفية", + "jobCategory": "تصنيف الوظيفة", + "category": "فئة", + "employeeEmailAddress": "عنوان البريد الإلكتروني للموظف", + "payrollBranch": "فرع الرواتب", + "yourChangeHasBeenSavedSuccessfully": "تم حفظ التغيير الخاص بك بنجاح", + "code": "الرمز", + "unit": "وحدة", + "quantity": "كمية", + "dateRequired": "التاريخ المطلوب", + "lineStatus": "حالة البند", + "statusDate": "تاريخ الحالة", + "transactionType": "نوع المعاملة", + "operatingUnit": "الوحدة التشغيلية", + "organizationCode": "رمز المنشأة", + "organization": "المنشأة", + "fromSubInventory": "من المخزون الفرعي", + "fromLocator": "من موقع التخزين", + "toSubInventory": "إلى المخزون الفرعي", + "toLocator": "إلى موقع التخزين", + "shipToLocator": "موقع التخزين المستلم", + "itemHistory": "سجل الصنف", + "mfg": "الشركة المصنّعة", + "lineType": "نوع البند", + "price": "السعر", + "lineAmount": "قيمة البند", + "lineDiscount": "خصم البند (%)", + "needByDate": "تاريخ الحاجة", + "promisedDate": "تاريخ التسليم المتعهد به", + "deliverToLocation": "تسليم إلى الموقع", + "requisitionNumber": "رقم الطلب", + "requester": "مقدم الطلب", + "quotationAnalysis": "تحليل عروض الأسعار", + "subject": "موضوعات", + "description": "وصف", + "pleaseEnterDescription": "الرجاء إدخال الوصف", + "supplier": "المورد", + "site": "موقع", + "buyer": "مشتر", + "preparer": "معد", + "creationDate": "تاريخ الإنشاء", + "shipToLocation": "الشحن الى الموقع", + "quotationNumber": "رقم عرض السعر", + "quotationDate": "تاريخ عرض السعر", + "paymentTerms": "شروط الدفع", + "currency": "عملة", + "grossAmount": "إجمالي المبلغ", + "discountAmount": "مبلغ الخصم", + "customDuty": "الرسوم الجمركية", + "shipHandle": "رسوم الشحن والمناولة", + "otherCharges": "رسوم أخرى", + "totalPOAmountWithVAT": "إجمالي مبلغ الشراء مع ضريبة القيمة المضافة", + "totalPOAmountInWords": "إجمالي قيمة أمر الشراء كتابةً", + "requestNumber": "رقم الطلب", + "uom": "وحدة القياس", + "operatingCode": "كود التشغيل", + "poNumber": "رقم أمر الشراء", + "revision": "مراجعة", + "quantityOrdered": "الكمية المطلوبة", + "quantityReceived": "الكمية المستلمة", + "bonusQuantity": "الكمية المجانية", + "purchasePrice": "سعر الشراء", + "discountPer": "خصم ٪", + "balanceQuantity": "الكمية المتبقية", + "netPrice": "السعر الصافي", + "closureStatus": "حالة الإغلاق", + "quotationNetPrice": "صافي سعر عرض السعر", + "quotationUOM": "وحدة القياس في عرض السعر", + "quotationQty": "الكمية في عرض السعر", + "itemCode": "رمز الصنف", + "vendorName": "اسم البائع", + "quotationMFGPartNumber": "رقم قطعة الشركة المصنّعة في عرض السعر", + "quotationDeliveryDate": "تاريخ تسليم عرض الأسعار", + "quotationBonusQuantity": "الكمية المجانية في عرض السعر", + "quotationLineTotal": "إجمالي بند عرض السعر", + "rfqUOM": "وحدة القياس لطلب عرض السعر", + "rfqQty": "الكمية المطلوبة لعرض السعر", + "rfqNumber": "رقم طلب عرض السعر", + "human": "الموارد", + "resources": "البشرية", + "details": "تفاصيل", + "noDataAvailable": "لا تتوافر بيانات", + "productName": "اسم المنتج", + "productDescription": "وصف المنتج", + "unitPrice": "سعر الوحدة", + "manufacturerName": "اسم المصنع", + "manufacturerPartName": "اسم قطعة الشركة المصنّعة", + "supplierName": "اسم المورد", + "supplierContact": "بيانات التواصل مع المورد", + "chargeToPatient": "يُحمّل على حساب المريض", + "justification": "التبرير", + "itemDescription": "وصف السلعة", + "groupCode": "كود المجموعة", + "primaryUOM": "وحدة القياس الأساسية", + "subgroupDescription": "وصف المجموعة الفرعية", + "subgroupCode": "رمز المجموعة الفرعية", + "groupDescription": "وصف المجموعة", + "templateName": "اسم القالب", + "itemCreationStatus": "حالة إنشاء الصنف", + "standardizationApprovalStatus": "حالة اعتماد التوحيد القياسي", + "standardizationApprovalRejectionReason": "سبب رفض اعتماد التوحيد القياسي", + "analyzedBy": "تحليل بواسطة", + "approvedDate": "تاريخ الموافقة", + "itemType": "نوع العنصر", + "relatedTo": "متعلق ب", + "requestDate": "تاريخ الطلب", + "analyzedDate": "تاريخ التحليل", + "urgent": "العاجلة", + "requestDetails": "تفاصيل الطلب", + "approvalLevel": "مستوى الموافقة", + "requesterDetails": "تفاصيل مقدم الطلب", + "myAttendance": "حضوري", + "workOnBreak": "التعويض عن العمل أثناء الاستراحة", + "next": "التالي", + "apply": "تقديم الطلب", + "mobile": "الجوال", + "completingYear": "نقدّر لك إتمام مدة خدمة", + "year": "سنة", + "month": "شهر", + "day": "يوم", "address": "العنوان", - "contactDetails": "بيانات التواصل", - "familyDetails": "تفاصيل عائلية", - "effectiveDate": "تاريخ النفاذ", - "country": "دولة" - }, - "clicked": { - "zero": "You clicked {} times!", - "one": "You clicked {} time!", - "two": "You clicked {} times!", - "few": "You clicked {} times!", - "many": "You clicked {} times!", - "other": "You clicked {} times!" - }, - "gender": { - "male": "Hi man ;) ", - "female": "Hello girl :)", - "with_arg": { - "male": "Hi man ;) {}", - "female": "Hello girl :) {}" - } - }, - "reset_locale": "إعادة ضبط اللغة", - "chat": "دردشة", - "mychats": "دردشاتي", - "advancedSearch": "بحث متقدم", - "openNot": "التبليغات المفتوحة", - "fyi": "تبليغات للعلم", - "toDo": "تبليغات الأعمال", - "all": "كل التبليغات", - "meNot": "تبليغات صادرة مني", - "view": "عرض", - "fromUserName": "من", - "sentDate": "تاريخ الإرسال", - "itemTypeDisplayName": "اسم العرض", - "none": "بدون", - "createNewChat": "إنشاء محادثة جديدة", - "brainMarathon": "ماراثون الدماغ", - "contestTopicAbout": "سيكون موضوع المسابقة حول:", - "gameDate": "تاريخ اللعبة:", - "gameTime": "وقت اللعب:", - "joinMarathon": "انضم إلى ماراثون", - "joinDemoMarathon": "انضم إلى الماراثون التجريبي", - "demo": "تجريبي", - "minutes": "الدقائق", - "seconds": "ثواني", - "note": "ملحوظة:", - "demoMarathonNoteP1": "يمكنك لعب ماراثون العرض لتتعلم كيف يعمل. يمكنك الانضمام إلى ماراثون", - "demoMarathonNoteP2": "خمس دقائق", - "demoMarathonNoteP3": "قبل الوقت الفعلي.", - "sponsoredBy": "برعاية:", - "question": "سؤال", - "marathoners": "الماراثون", - "marathoner": "ماراثونر", - "prize": "جائزة:", - "winnerSelection": "اختيار الفائز", - "qualifiers": "تصفيات", - "qualifier": "المؤهل", - "getReadyForContest": "استعد للمسابقة القادمة:", - "winnerSelectedRandomly": "سيتم اختيار الفائز عشوائياً من بين التصفيات.", - "fingersCrossed": "تشابك الاصابع!!!", - "congrats": "مبروك !!!", - "allQuestionsCorrect": "لقد أجبت على جميع الأسئلة بشكل صحيح.", - "otp": "OTP", - "verification": "تَحَقّق", - "resend": "إعادة إرسال", - "codeExpire": "انتهت صلاحية رمز التحقق", - "typeheretoreply": "اكتب هنا للرد", - "favorite": "مفضلتي", - "searchfromchat": "البحث من الدردشة", - "yourAnswerCorrect": "إجابتك صحيحة", - "youMissedTheQuestion": "نفد منك الوقت. أنت خارج اللعبة. لكن يمكنك الاستمرار وكمشاهد.", - "wrongAnswer": "إجابتك غير صحيحة. أنت خارج اللعبة. لكن يمكنك الاستمرار وكمشاهد.", - "oops": "أوه!!!", - "winner": "الفائز", - "youWantToLeaveMarathon": "هل أنت متأكد أنك تريد العودة؟ سوف تخرج من المسابقة.", - "ourSponsor": "راعينا:", - "startingIn": "يبدأ في", - "youAreOutOfContest": "أنت خارج المسابقة.", - "winners": "الفائزين!!!", - "expireAfter": "تنتهي بعد", - "oneWeek": "أسبوع 1", - "twoWeek": "2 أسبوع", - "noUpcoming": "لا يوجد قادم", - "fakeLocation": ".لقد تتبعنا أنك تحاول استخدام موقع مزيف! يعتبر هذا مخالفة وقد تم إخطار الموارد البشرية", - "noWinner": "حزين! لم يفز أحد اليوم.", - "myTeam": "فريقي", - "youCanPlayDemo": "لكن يمكنك لعب العرض", - "group": "مجموعة", - "searchGroup": "مجموعة البحث", - "connectHmgWifi": "قم بتوصيل HMG WIFI", - "connectedHmgWifi": "اتصال HMG WIFI", - "itgForms": "نماذج (ITG)", - "resetAdPassword": "إعادة تعيين كلمة مرور AD", - "myDocuments": "مستنداتي", - "requiredDocuments": "المستندات المطلوبة", - "optionalDocuments": "المستندات الاختيارية", - "allDocuments": "كافة المستندات", - "expiredDocuments": "المستندات منتهية الصلاحية", - "missingDocuments": "مستندات مفقودة", - "uploadedDocuments": "المستندات التي تم تحميلها", - "addAtLeastOneAttachment": "الرجاء إضافة مرفق واحد على الأقل.", - "pleaseClickButtonToJoinMarathon": "الرجاء الضغط على الزر أدناه للانضمام إلى الماراثون", - "youCannotJoinTheMarathon": "لا يمكنك الانضمام إلى الماراثون لأنك تجاوزت الحد الزمني", - "open": "يفتح", - "paymentRequest": "طلب الدفع", - "paymentDetails": "تفاصيل الدفع", - "requestNo": "رقم الطلب", - "requesterEmpNum": "رقم الموظف الذي قدم الطلب", - "requesterEmpName": "اسم الموظف مقدم الطلب", - "prepareEmpName": "إعداد اسم الموظف", - "requesterPositionName": "اسم الوظيفة لمقدم الطلب", - "preparePositionName": "إعداد اسم الوظيفة", - "requesterPayrollName": "اسم مقدم الطلب", - "payingORGName": "اسم المنظمة الدافعة", - "requestAmount": "مبلغ الطلب", - "typeofPayment": "نوع الدفع", - "beneficiaryDetails": "تفاصيل المستفيد", - "beneficiaryName": "اسم المستفيد", - "idIqama": "الهوية السعودية / الإقامة", - "beneficiaryBankName": "اسم البنك المستفيد", - "sadadNumber": "رقم سداد", - "beneficiaryIBAN": "رقم IBAN للمستفيد", - "purchaseOrders": "طلبات الشراء", - "approvalDate": "تاريخ الموافقة", - "pOAmount": "مبلغ الطلب", - "versionStatus": "حالة الإصدار", - "supplierNo": "رقم المورد", - "general": "عام", - "requesterOperatingUnit": "وحدة تشغيل مقدم الطلب", - "prepareEmpNum": "إعداد رقم الموظف", - "supplierInfo": "معلومات المورد", - "supplierAcNo": "رقم حساب المورد", - "supplierAcName": "اسم حساب المورد", - "supplierIBAN": "رقم IBAN للمورد", - "supplierCRNo": "رقم السجل التجاري", - "suppliedAcNo": "رقم الحساب المقدم", - "patientRefundInvoice": "فاتورة استرداد الأموال للمريض", - "patientNumber": "رقم المريض", - "patientName": "اسم المريض", - "invoiceDate": "تاريخ الفاتورة", - "refundInvoice": "فاتورة الاسترجاع", - "hospitalClinic": "عيادة المستشفى", - "graphicalAnalysis": "التحليل الرسومي", - "itemHistoryAnalysis": "تحليل تاريخ العنصر", - "pOno": "امر شراء #", - "oprUnit": "وحدة التشغيل", - "qtyOrdered": "الكمية المطلوبة", - "qtyReceived": "الكمية المستلمة", - "bonusQty": "كمية المكافأة", - "balQty": "كمية التوازن", - "videoCall": "مكالمة فيديو", - "addUsers": "إضافة مستخدمين إلى المجموعة", - "admin": "مشرف", - "areYouSureWantTodelete": "هل أنت متأكد أنك تريد الحذف؟", - "Attendance": "الحضور", - "audioCall": "مكالمة صوتية", - "editGroups": "تعديل المجموعة", - "enterGroupName": "يرجى إدخال اسم المجموعة الصحيح", - "enterGroupNamePlease": "يرجى إدخال اسم المجموعة", - "groupMembers": "أعضاء المجموعة", - "groupName": "اسم المجموعة", - "groupNameshouldbe": "يجب أن يتكون اسم المجموعة من 10 أحرف على الأقل", - "manage": "إدارة", - "manageGroup": "إدارة المجموعة", - "members": "الأعضاء", - "searchByUserName": "البحث بواسطة اسم المستخدم", - "shareScreen": "مشاركة الشاشة", - "start": "يبدأ", - "about": "عن", - "explore": "يستكشف", - "mazaya": "مازيا", - "benefits": "فوائد", - "mazayaDesc": "اكتشف الخصومات والعروض الخاصة المتاحة للموظفين", - "viewallofferMazaya": "أعرض كل المزايا", - "buyerName": "اسم المشتري", - "buyerNumber": "رقم المشتري", - "highestBidder": "أعلى مزايد", - "remarks": "ملاحظات", - "faHeader": "تفاصيل رأس الصفحة FA", - "bookTypeCode": "رمز نوع الكتاب", - "categoryCode": "رمز الفئة", - "categoryGroup": "مجموعة الفئات", - "faLINES": "تفاصيل الأصول", - "assetNumber": "رقم الأصول", - "assetDescription": "وصف الأصول", - "barCodeNumber": "رقم الباركود", - "datePlaceInService": "تاريخ وضعه في الخدمة", - "serialNumber": "رقم سري", - "disposedDate": "تاريخ التخلص", - "netBookValue": "القيمة الدفترية الصافية", - "purchasedPrice": "سعر الشراء", - "usefulLife": "العمر الإنتاجي", - "yearsUsed": "سنوات الاستخدام", - "faRequest": "طلب التخلص من FA", - "showMore": "عرض المزيد", - "ticketDetails": "تفاصيل التذكرة", - "currentTicketBalance": "رصيد التذكرة الحالي", - "ticketHistory": "تاريخ التذكرة", - "proceedBooking": "المتابعة للحجز", - "courses": "الدورات التدريبية", - "course": "دورة", - "status": "الحالة", - "deadline": "الموعد النهائي", - "overdue": "متأخر", - "noCoursesAvailable": "لا توجد دورات متاحة", - "pullToRefresh": "اسحب للتحديث", - "courseUrlNotAvailable": "رابط الدورة غير متوفر", - "etqanOVR": "الإبلاغ عن حادثة", - "etqanRequest": "طلبات الحوادث", - "selectProject": "حدد المشروع", - "project": "مشروع", - "keen": "حريص", - "reportIncident": "الإبلاغ عن الحادث", - "myIncidents": "حوادثي", - "incidentDetails": "تفاصيل الحادث", - "ticketNumber": "رقم التذكرة", - "ticketId": "معرف التذكرة", - "pleaseSelectProject": "الرجاء اختيار مشروع", - "viewAttachment": "عرض المرفق", - "parkingQr": "وقوف السيارات QR", - "startNow": "ابدأ الآن", - "destination_type": "نوع الوجهة", - "source_organization_name": "اسم منظمة المصدر", - "destination_organization_name": "اسم منظمة الوجهة", - "costCenter": "مركز التكلفة", - "priceSAR": "السعر (ريال سعودي)", - "amountSAR": "المبلغ (ريال سعودي)", - "amuLastThreeMonths": "AMU (آخر 3 أشهر)", - "buyerDetails": "تفاصيل المشتري", - "chatBot": "المساعد الذكي", - "helloAskMe": "مرحباً، اسألني", - "anything": "عن أي شيء...", - "thinking": "جاري التفكير", - "waitingForResponse": "في انتظار الرد...", - "typeAMessage": "اكتب رسالة...", - "initializingChatPleaseWait": "يرجى الانتظار، جاري تهيئة المحادثة...", - "am": "ص", - "pm": "م" -}; -static const Map _en_US = { - "mohemm": "Mohemm", - "english": "English", - "arabic": "عربي", - "login": "Login", - "pleaseEnterLoginDetails": "Please enter the detail below to login", - "username": "Username", - "password": "Password", - "welcomeBack": "Welcome back", - "wouldYouLikeToLoginWithCurrentUsername": "Would you like to login with current Username?", - "lastLoginDetails": "Last Login Details:", - "verificationType": "Verification Type:", - "pleaseVerify": "Please Verify", - "pleaseVerifyForBio": "Please verify login with one of the following options", - "verifyThroughFace": "Verify Through Face", - "verifyThroughFingerprint": "Verify Through Fingerprint", - "verifyThroughSMS": "Verify Through SMS", - "verifyThroughWhatsapp": "Verify Through Whatsapp", - "useAnotherAccount": "Use Another Account", - "pleaseEnterTheVerificationCodeSentTo": "Please enter the verification code sent to ", - "theVerificationCodeWillExpireIn": "The verification code will expire in ", - "goodMorning": "Good Morning", - "markAttendance": "Mark Attendance", - "timeLeftToday": "Time Left Today", - "checkIn": "Check In", - "workList": "Work List", - "leaveBalance": "Leave Balance", - "missingSwipes": "Missing Swipes", - "ticketBalance": "Ticket Balance", - "other": "Other", - "services": "Services", - "viewAllServices": "View All Services", - "monthlyAttendance": "Monthly Attendance", - "vacationRule": "Vacation Rule", - "vacationType": "Vacation Type", - "startDateT": "Start Date", - "endDateT": "End Date", - "workFromHome": "Work From Home", - "ticketRequest": "Ticket Request", - "viewAllOffers": "View All Offers", - "offers": "Offers & ", - "discounts": "Discounts", - "newString": "New", - "setTheNewPassword": "Set the new password", - "typeYourNewPasswordBelow": "Type your new password below", - "typeYourNewActiveDirectoryPasswordBelow": "Type new active directory password below", - "confirmPassword": "Confirm Password", - "update": "Update", - "title": "Title", - "home": "Home", - "mySalary": "My Salary", - "createRequest": "Create Request", - "forgotPassword": "Forgot Password", - "employeeId": "Employee ID", - "loginCodeWillSentToMobileNumber": "Please Enter your Employee ID, A login code will be sent to your mobile number", - "changePassword": "Change Password", - "ok": "OK", - "confirm": "Confirm", - "passwordChangedSuccessfully": "Password changed successfully", - "itemsForSale": "Items for Sale", - "attendanceDetails": "Attendance Details", - "order": "order", - "earlyOut": "Early Out", - "shortage": "Shortage", - "excess": "Excess", - "lateIn": "Late In", - "approvedCheckOut": "Approved Check Out", - "approvedCheckIn": "Approved Check In", - "actualCheckOut": "Actual Check Out", - "actualCheckIn": "Actual Check In", - "present": "PRESENT", - "pres": "present", - "shiftTime": "Shift Time", - "absent": "ABSENT", - "attendance": "Attendance", - "scheduleDays": "Schedule\nDays", - "offDays": "Off\nDays", - "nonAnalyzed": "Non\nAnalyzed", - "shortageHour": "Shortage\nHour", - "stats": "Stats", - "completed": "Completed", - "doNotUseRecentPassword": "Do not use recent password", - "atLeastOneLowercase": "At least one lowercase", - "atLeastOneUppercase": "At least one uppercase", - "atLeastOneNumeric": "At least one numeric", - "minimum8Characters": "Minimum 8 characters", - "doNotAddRepeatingLetters": "Do not add repeating letters", - "itShouldContainSpecialCharacter": "It should not contain special characters", - "confirmPasswordMustMatch": "Confirm password must match", - "sms": "SMS", - "fingerPrint": "Fingerprint", - "face": "Face", - "whatsapp": "Whatsapp", - "reject": "Reject", - "approve": "Approve", - "generate": "Generate", - "cancel": "Cancel", - "requestedItems": "Requested Items", - "request": "Request", - "myRequest": "My Request", - "actions": "Actions", - "delegate": "Delegate", - "request_info": "Request Info", - "attachments": "Attachments", - "info": "Info.", - "employeeNumber": "Employee Number", - "assignmentNumber": "Assignment Number", - "employeeName": "Employee Name", - "scheduleDate": "Schedule Date", - "shiftType": "Shift Type", - "shift": "Shift", - "breakText": "Break", - "actualSwipeStart": "Actual Swipe Start", - "actualSwipeEnd": "Actual Swipe End", - "approvedSwipeStart": "Approved Swipe Start", - "approvedSwipeStartReason": "Approved Swipe Start Reason", - "approvedSwipeEnd": "Approved Swipe End", - "approvedSwipeEndReason": "Approved Swipe End Reason", - "from": "From", - "to": "To", - "sent": "Sent", - "closed": "Closed", - "id": "ID", - "responder": "Responder", - "jobTitle": "Job Title", - "grade": "Grade", - "jobCategory": "Job Category", - "category": "Category", - "employeeEmailAddress": "Employee Email Address", - "payrollBranch": "Payroll Branch", - "yourChangeHasBeenSavedSuccessfully": "Your change has been saved successfully", - "code": "Code", - "unit": "Unit", - "quantity": "Quantity", - "dateRequired": "Date Required", - "lineStatus": "Line Status", - "statusDate": "Status Date", - "transactionType": "Transaction Type", - "operatingUnit": "Operating Unit", - "organizationCode": "Organization Code", - "organization": "Organization", - "fromSubInventory": "From Sub Inventory", - "fromLocator": "From Locator", - "toSubInventory": "To Sub Inventory", - "toLocator": "To Locator", - "shipToLocator": "Ship To Locator", - "itemHistory": "Item History", - "mfg": "MFG", - "lineType": "Line Type", - "price": "Price", - "lineAmount": "Line Amount", - "lineDiscount": "Line Discount %", - "needByDate": "Need By Date", - "promisedDate": "Promised Date", - "deliverToLocation": "Deliver To Location", - "requisitionNumber": "Requisition Number", - "requester": "Requester", - "subject": "Subject", - "quotationAnalysis": "Quotation Analysis", - "description": "Description", - "pleaseEnterDescription": "Please enter description", - "supplier": "Supplier", - "site": "Site", - "buyer": "Buyer", - "preparer": "Preparer", - "creationDate": "Creation Date", - "shipToLocation": "Ship To Location", - "quotationNumber": "Quotation Number", - "quotationDate": "Quotation Date", - "paymentTerms": "Payment Terms", - "currency": "Currency", - "grossAmount": "Gross Amount", - "discountAmount": "Discount Amount", - "customDuty": "Custom Duty", - "shipHandle": "Ship Handle", - "otherCharges": "Other Charges", - "totalPOAmountWithVAT": "Total PO Amount With VAT", - "totalPOAmountInWords": "Total PO Amount In Words", - "requestNumber": "Request Number", - "uom": "UOM", - "operatingCode": "Operating Code", - "poNumber": "PO Number", - "revision": "Revision", - "quantityOrdered": "Quantity Ordered", - "quantityReceived": "Quantity Received", - "bonusQuantity": "Bonus Quantity", - "purchasePrice": "Purchase Price", - "discountPer": "Discount %", - "balanceQuantity": "Balance Quantity", - "netPrice": "Net Price", - "closureStatus": "Closure Status", - "quotationNetPrice": "Quotation Net Price", - "quotationUOM": "Quotation UOM", - "quotationQty": "Quotation Qty", - "itemCode": "item Code", - "vendorName": "Vendor Name", - "quotationMFGPartNumber": "Quotation MFG Part Number", - "quotationDeliveryDate": "Quotation Delivery Date", - "quotationBonusQuantity": "Quotation Bonus Quantity", - "quotationLineTotal": "Quotation Line Total", - "rfqUOM": "RFQ UOM", - "rfqQty": "RFQ Qty", - "rfqNumber": "RFQ Number", - "msg": "Hello {} in the {} world ", - "msg_named": "{} are written in the {lang} language", - "clickMe": "Click me", - "human": "Human", - "resources": "Resources", - "details": "Details", - "noDataAvailable": "No Data Available", - "productName": "Product Name", - "productDescription": "Product Description", - "unitPrice": "Unit Price", - "manufacturerName": "Manufacturer Name", - "manufacturerPartName": "Manufacturer Part Name", - "supplierName": "Supplier Name", - "supplierContact": "Supplier Contact", - "chargeToPatient": "Charge To Patient", - "justification": "Justification", - "itemDescription": "Item Description", - "groupCode": "Group Code", - "primaryUOM": "Primary UOM", - "subgroupDescription": "Subgroup Description", - "subgroupCode": "Subgroup Code", - "groupDescription": "Group Description", - "templateName": "Template Name", - "itemCreationStatus": "Item Creation Status", - "standardizationApprovalStatus": "Standardization Approval Status", - "standardizationApprovalRejectionReason": "Standardization Approval Rejection Reason", - "analyzedBy": "Analyzed By", - "approvedDate": "Approved Date", - "itemType": "Item Type", - "relatedTo": "Related To", - "requestDate": "Request Date", - "analyzedDate": "Analyzed Date", - "urgent": "Urgent", - "requestDetails": "Request Details", - "approvalLevel": "Approval Level", - "requesterDetails": "Requester Details", - "myAttendance": "My Attendance", - "workOnBreak": "Work On Break", - "next": "Next", - "apply": "Apply", - "mobile": "Mobile", - "year": "Year", - "month": "Month", - "day": "Day", - "completingYear": "We appreciate you for completing the service of", - "address": "Address", - "phoneNumber": "Phone Number", - "businessGroup": "Business", - "Payroll": "Payroll", - "civilIdentityNumber": "Civil Identity Number", - "dateOfBirth": "Date of Birth", - "maritalStatus ": "Marital Status ", - "fullName": "Full Name", - "remove": "Remove", - "Attendance": "Attendance", - "submit": "Submit", - "submitAsAnonymous": "Submit as Anonymous", - "areYouSureYouWantToSubmit": "Are you sure you want to submit?", - "comments": "Comments", - "writeComment": "Write a comment", - "approversList": "Approvers List", - "yourRequestHasBeenSubmittedForApprovals": "Your request has been submitted for approvals", - "monthlyPaySlip": "Monthly Pay Slip", - "particular": "Particular", - "earnings": "Earnings", - "deductions": "Deductions", - "paymentMethodName": "Payment Method Name", - "bankName": "Bank Name", - "branchCode": "Branch Code", - "accountNo": "Account No", - "summaryOfInformation": "Summary of Information", - "totalPayAmount": "Total Pay Amount", - "paymentInformation": "Payment Information", - "amount": "Amount", - "correctCurrentDatails": "correct or complete the current details", - "selectType": "Select the type of change you want to make", - "enterNewInfo": "Enter new Information because of a real change to the current details (e.g because of a change in marital status)", - "endDate": "*End Date", - "removeThisMember": "Are You Sure You Want to Remove this Member?", - "wantUpdateThisMember": "Are You Sure You Want to Update this Member?", - "addNewFamilyMember": "Add New Family Member", - "addRow": "Add new row", - "pleaseSelect": "Please Select *", - "delete": "delete", - "add": "Add", - "edit": "Edit", - "myProfile": "My Profile", - "mowadhafhi": "Mowadhafi", - "searchAnnouncements": "Search Announcements", - "announcements": "Announcements", - "swipeRequest": "Swipe Request", - "serviceType": "Service Type", - "departmentName": "Department Name", - "selectDepartment": "Select Department", - "relatedSection": "Related Section", - "selectSection": "Select Section", - "relatedTopic": "Related Topic", - "selectTopic": "Select Topic", - "supportingDocument": "Supporting Document", - "mowadhafhiRequest": "Mowadhafi Request", - "ticketReference": "Ticket Reference", - "section": "Section", - "topic": "Topic", - "actionBy": "Action By", - "pending": "Pending", - "pendingTransactions": "Pending Transactions", - "selectRequestType": "Please select request type", - "dateFrom": "Date From", - "dateTo": "Date To", - "requestName": "Request Name", - "createdFor": "Created For", - "requestType": "Request Type", - "requestCreatedSuccessfully": "Request created successfully", - "search": "Search", - "wantToReject": "Are you sure want to reject?", - "employeeDigitalID": "Employee Digital ID", - "businessCard": "Business Card", - "checkOut": "Check Out", - "regular": "Regular", - "mark": "Mark", - "performance": "Performance Evaluation", - "performanceEvaluationIn": "Your performance Evaluation in", - "valuationIn": "Performance Evaluation in", - "viewBusinessCard": "View Business Card", - "performanceEvaluation": "Performance Evaluation", - "logout": "Logout", - "selectMethodOfAttendance": "Select the method to mark the attendance", - "comeNearHMGWifi": "Please come near to HMG wifi", - "deliverNotificationToMeRegardless": "Deliver notifications to me regardless of any general rules", - "close": "Close", - "respond": "Respond", - "vacationRuleAdded": "Vacation rule added", - "selectTypeT": "Select Type", - "notification": "Notification", - "selectNotification": "Select Notification", - "ifAllSelectedYouWillSkip": "*If All is selected, you will skip to step 3", - "applyForVacationRule": "Apply for Vacation Rule", - "step1": "Step 1", - "step2": "Step 2", - "step3": "Step 3", - "message": "Message", - "writeAMessage": "Write a message", - "notificationReassign": "Notification Reassign", - "selectEmployee": "Select Employee", - "searchEmployeeForReplacement": "Search employee for replacement", - "searchForEmployee": "Search for Employee", - "pleaseSpecifyEndTime": "Please specify End Time", - "pleaseSelectNotificationReassign": "Please select notification reassign", - "pleaseSelectEmployeeForReplacement": "Please select employee for replacement", - "pleaseSelectAction": "Please select action", - "pleaseSelectDate": "Please select date", - "todayAttendance": "Today's Attendance", - "viewAttendance": "View Attendance", - "teamMembers": "Team Members", - "profileDetails": "Profile Details", - "noResultsFound": "No Results Found", - "searchBy": "Search by", - "myTeamMembers": "My Team Members", - "save": "Save", - "TurnNotificationsFor": "Turn on notifications for", - "worklistSettings": "Worklist Settings", - "absenceType": "Absence Type", - "absenceCategory": "Absence Category", - "days": "Days", - "hours": "Hours", - "approvalStatus": "Approval Status", - "absenceStatus": "Absence Status", - "poweredBy": "Powered By", - "cloudSolutions": "Cloud Solutions", - "subordinateLeave": "Subordinate Leave", - "numberDays": "Number of days", - "selectTemplate": "Select Template", - "myPostedAds": "My posted ads", - "browseCategories": "Browse Categories", - "searchItems": "Search Items", - "offerAndDiscounts": "Offer & Discounts", - "offerValid": "Offer Valid", - "offerExpired": "Offer Expired", - "whatAreYouOffering": "What are you offering?", - "selectCategory": "Select Category", - "inProgress": "InProgress", - "locked": "Locked", - "addDetails": "Add Details", - "reviewAndSell": "Review & Sell", - "itemTitle": "Item Title", - "itemCondition": "Item Condition", - "used": "Used", - "region": "Region", - "selectRegion": "Select Region", - "itemPrice": "Item Price", - "itemPhotos": "Item Photos", - "itemInfo": "Item Info", - "uploadAttachment": "Upload Attachment", - "selectFromGalleryOrOpenCamera": "Select from gallery or open camera", - "openCamera": "Open\nCamera", - "uploadFromGallery": "Upload from\nGallery", - "name": "Name", - "email": "Email", - "noHistoryAvailable": "No History Available", - "purchaseRequisition": "Purchase Requisition", - "moveOrder": "Move Order", - "humanResource": "Human Resource", - "purchaseOrder": "Purchase Order", - "ITGForms": "ITG Forms", - "itemCreation": "Item Creation", - "stamp": "Stamp", - "addFavoriteList": "Do you want to add {name} in your favorite list", - "feedbackUserExperience": "This is to get the feedback about the user experience", - "rateUI": "How would you like to rate", - "rateUI2": "How do you satisfied with this application", - "submitSurvey": "Submit Survey", - "typeHere": "Type here", - "infoDetail": "Info Detail", - "amount_detail": "Amount Detail", - "currentBalance": "Current Balance", - "currentLeaveBalance": "Current Leave Balance", - "calculatedDays": "Calculated Days", - "totalDays": "Total Days", - "usedBalance": "Used", - "infants": "Infants", - "child": "Child", - "adult": "Adult", - "updateMember": "Are You Sure You Want to Update this Member?", - "fieldIsEmpty": "'{data}' Field is empty. Please select", - "pleaseEnterComments": "Please enter comments", - "skip": "Skip", - "typeCurrentPasswordBelow": "Type Your Current password below", - "currentPassword": "Current password", - "concurrentReports": "Concurrent Reports", - "EnterNewAddressMoved": "Enter a new address if you have moved", - "CorrectAddress": "Correct or amend this address", - "SelectChangeWantToMake": "Select the type of change you want to make", - "profile": { - "reset_password": { - "label": "Reset Password", - "username": "Username", - "password": "password" + "phoneNumber": "رقم الجوال", + "businessGroup": "مجموعة العمل", + "Payroll": "الرواتب", + "civilIdentityNumber": "رقم الهويه", + "dateOfBirth": "تاريخ الميلاد", + "maritalStatus ": "الحالة الاجتماعية", + "fullName": "الاسم الكامل", + "remove": "حذف", + "submit": "إرسال", + "submitAsAnonymous": "إرسال دون الإفصاح عن الهوية", + "areYouSureYouWantToSubmit": "هل أنت متأكد من إرسال الطلب؟", + "comments": "تعليقات", + "writeComment": "أكتب تعليقا", + "approversList": "قائمة المعتمدين", + "yourRequestHasBeenSubmittedForApprovals": "تم إرسال طلبك للاعتماد", + "monthlyPaySlip": "قسيمة الراتب الشهرية", + "particular": "البيان", + "earnings": "المستحقات", + "deductions": "الاستقطاعات", + "paymentMethodName": "اسم طريقة الدفع", + "bankName": "اسم البنك", + "branchCode": "رمز الفرع", + "accountNo": "رقم الحساب", + "summaryOfInformation": "ملخص المعلومات", + "totalPayAmount": "المبلغ الإجمالي للدفع", + "paymentInformation": "معلومات الدفع", + "performance": "تقييم الأداء", + "performanceEvaluation": "تقييم الأداء في", + "performanceEvaluationIn": "تقييم أدائك في", + "valuationIn": "تقييم الأداء في", + "amount": "المبلغ", + "correctCurrentDatails": "تصحيح البيانات الحالية أو استكمالها", + "selectType": " حدد نوع التغيير الذي تريد القيام به", + "enterNewInfo": + " أدخل معلومات جديدة بسبب تغيير حقيقي في التفاصيل الحالية (على سبيل المثال بسبب تغيير في الحالة الاجتماعية", + "endDate": "تاريخ الانتهاء", + "removeThisMember": "هل أنت متأكد من إزالة هذا الفرد؟", + "wantUpdateThisMember ": "هل أنت متأكد من تحديث بيانات هذا الفرد؟", + "addNewFamilyMember": "اضافة عضو جديد", + "addRow": "اضافة صف جديد", + "pleaseSelect": "الرجاء اختيار", + "delete": "حذف", + "edit": "تعديل", + "add": "اضافه", + "myProfile": "ملفي الشخصي", + "mowadhafhi": "موظفي", + "searchAnnouncements": "البحث في الإعلانات", + "announcements": "اعلانات", + "swipeRequest": "طلب تصحيح البصمة", + "serviceType": "نوع الخدمه", + "departmentName": "اسم القسم", + "selectDepartment": "اختر القسم", + "relatedSection": "قسم ذو صله", + "selectSection": "اختيار القسم", + "relatedTopic": "عنوان ذو صله", + "selectTopic": "اختر العنوان", + "supportingDocument": "ارفاق مستند", + "mowadhafhiRequest": "طلب موظفي", + "ticketReference": "الرقم المرجعي للتذكرة", + "section": "القسم", + "topic": "العنوان", + "actionBy": "الإجراء بواسطة", + "pending": "قيد الانتظار", + "pendingTransactions": "المعاملات قيد الانتظار", + "selectRequestType": "الرجاء اختيار نوع الطلب", + "dateFrom": "من تاريخ", + "dateTo": "الى تاريخ", + "requestName": "اسم الطلب", + "createdFor": "تم الإنشاء لصالح", + "requestCreatedSuccessfully": "تم انشاء الطلب بنجاح", + "search": "بحث", + "filterEnterPrDetailsHere": "ادخل تفاصيل طلب الشراء هنا", + "filterEnterPoDetailsHere": "ادخل تفاصيل أمر الشراء هنا", + "wantToReject": "هل أنت متأكد من رفض الطلب؟", + "requestType": "نوع الطلب", + "employeeDigitalID": "الهوية الرقمية للموظف", + "businessCard": "بطاقة العمل", + "viewBusinessCard": "عرض بطاقة العمل", + "logout": "تسجيل خروج", + "checkOut": "تسجيل الانصراف", + "regular": "اعتيادي", + "mark": "تسجيل", + "selectMethodOfAttendance": "اختر طريقة تسجيل الحضور", + "comeNearHMGWifi": "يرجى الاقتراب من شبكة Wi-Fi الخاصة بالمجموعة", + "deliverNotificationToMeRegardless": + "تسليم الإخطارات إلي بغض النظر عن أي قواعد عامة", + "close": "أغلق", + "respond": "رد", + "vacationRuleAdded": "تمت إضافة قاعدة الإجازة", + "selectTypeT": "اختر صنف", + "notification": "تنبيه", + "selectNotification": "حدد إعلام", + "ifAllSelectedYouWillSkip": "* إذا تم تحديد الكل ، فستنتقل إلى الخطوة 3", + "applyForVacationRule": "طلب تطبيق قواعد الإجازات", + "step1": "الخطوة 1", + "step2": "الخطوة 2", + "step3": "الخطوه 3", + "message": "رسالة", + "writeAMessage": "اكتب رسالة", + "notificationReassign": "إعادة توجيه الإشعار", + "selectEmployee": "حدد الموظف", + "searchEmployeeForReplacement": "ابحث عن موظف بديل", + "searchForEmployee": "ابحث عن موظف", + "pleaseSpecifyEndTime": "الرجاء تحديد وقت الانتهاء", + "pleaseSelectNotificationReassign": "يرجى تحديد إعادة تعيين الإخطار", + "pleaseSelectEmployeeForReplacement": "الرجاء تحديد موظف للاستبدال", + "pleaseSelectAction": "الرجاء تحديد الإجراء", + "pleaseSelectDate": "الرجاء تحديد التاريخ", + "todayAttendance": "حضور اليوم", + "viewAttendance": "عرض الحضور", + "teamMembers": "أعضاء الفريق", + "profileDetails": "الملف الشخصي", + "noResultsFound": "لم يتم العثور على نتائج", + "searchBy": "بحث بواسطة", + "myTeamMembers": "أعضاء فريقي", + "save": "حفظ", + "TurnNotificationsFor": "تفعيل الاشعارات", + "worklistSettings": "اعدادات الاشعارات", + "absenceType": "نوع الغياب", + "absenceCategory": "تصنيف الغياب", + "days": "أيام", + "hours": "ساعات", + "approvalStatus": "حالة الموافقة", + "absenceStatus": "حالة الغياب", + "subordinateLeave": "إجازات الموظفين التابعين", + "numberDays": "عدد الأيام", + "poweredBy": "بدعم من", + "cloudSolutions": "الحلول السحابية", + "selectTemplate": "حدد قالب", + "myPostedAds": "إعلاناتي المنشورة", + "browseCategories": "تصفح الفئات", + "searchItems": "عناصر البحث", + "offerAndDiscounts": "العروض والخصومات", + "offerValid": "العرض ساري", + "offerExpired": "انتهى العرض", + "whatAreYouOffering": "ما الذي تعرضه؟", + "selectCategory": "اختر الفئة", + "inProgress": "قيد التنفيذ", + "locked": "مقفل", + "addDetails": "أضف التفاصيل", + "reviewAndSell": "المراجعة والنشر للبيع", + "itemTitle": "عنوان البند", + "itemCondition": "حالة السلعة", + "used": "مستعمل", + "region": "منطقة", + "selectRegion": "اختر المنطقة", + "itemPrice": "سعر السلعة", + "itemPhotos": "صور السلعة", + "itemInfo": "معلومات العنصر", + "uploadAttachment": "تحميل المرفق", + "selectFromGalleryOrOpenCamera": + "اختر صورة من المعرض أو التقط صورة بالكاميرا", + "openCamera": "فتح\nالكاميرا", + "uploadFromGallery": "تحميل من\nملفات الجهاز", + "name": "الأسم", + "email": "ايميل", + "noHistoryAvailable": "لايوجد سجل بيانات سابقة ", + "purchaseRequisition": "طلب شراء", + "moveOrder": "طلب تغيير", + "humanResource": "الموارد البشرية", + "purchaseOrder": "أمر شراء", + "ITGForms": "ITG نماذج", + "itemCreation": "إنشاء صنف", + "stamp": "ختم", + "addFavoriteList": "هل تريد اضافة {name} لقائمة المفضله", + "feedbackUserExperience": "هذا للحصول على تعليقات حول تجربة المستخدم", + "rateUI": "كيف تقيّم واجهة التطبيق؟", + "rateUI2": "ما مدى رضاك عن هذا التطبيق؟", + "submitSurvey": "ارسال الاستبيان", + "typeHere": "اكتب هنا", + "infoDetail": "تفاصيل المعلومات", + "amount_detail": "تفاصيل المبلغ", + "currentBalance": "الرصيد الحالي", + "currentLeaveBalance": "رصيد الإجازات الحالي", + "calculatedDays": "الأيام المحتسبة", + "totalDays": "مجموع الأيام", + "usedBalance": "المستخدم", + "infants": "رضيع", + "child": "طفل", + "adult": "بالغ", + "updateMember": "هل انت متأكد تريد تحديث بيانات هذا العضو؟", + "fieldIsEmpty": "'{data}' الحقل فارغ. الرجاء التحديد", + "pleaseEnterComments": "الرجاء إدخال التعليقات", + "skip": "يتخطى", + "typeCurrentPasswordBelow": "اكتب كلمة المرور الحاليه", + "currentPassword": "كلمة المرور الحاليه", + "concurrentReports": "التقارير المتزامنه", + "EnterNewAddressMoved": "أدخل عنوان جديد إذا كنت قد انتقلت", + "CorrectAddress": "تصحيح أو تعديل هذا العنوان", + "SelectChangeWantToMake": " حدد نوع التغيير الذي تريد القيام به.", + "profile": { + "reset_password": { + "label": "إعادة تعيين كلمة المرور", + "username": "اسم المستخدم", + "password": "كلمة المرور", + }, + "profileCompletionPer": "استكمال الملف الشخصي", + "completeProfile": "استكمال الملف الشخصي", + "personalInformation": "معلومات شخصية", + "basicDetails": "تفاصيل أساسية", + "address": "العنوان", + "contactDetails": "بيانات التواصل", + "familyDetails": "تفاصيل عائلية", + "effectiveDate": "تاريخ سريان التغيير", + "country": "دولة", + }, + "clicked": { + "zero": "You clicked {} times!", + "one": "You clicked {} time!", + "two": "You clicked {} times!", + "few": "You clicked {} times!", + "many": "You clicked {} times!", + "other": "You clicked {} times!", + }, + "gender": { + "male": "Hi man ;) ", + "female": "Hello girl :)", + "with_arg": {"male": "Hi man ;) {}", "female": "Hello girl :) {}"}, }, - "profileCompletionPer": "Profile Completion", - "completeProfile": "Complete Profile", - "personalInformation": "Personal Information", - "basicDetails": "Basic Details", + "reset_locale": "إعادة ضبط اللغة", + "chat": "دردشة", + "mychats": "دردشاتي", + "advancedSearch": "بحث متقدم", + "openNot": "الإشعارات المفتوحة", + "fyi": "إشعارات للاطلاع", + "toDo": "إشعارات تتطلب إجراء", + "all": "جميع الإشعارات", + "meNot": "الإشعارات الصادرة مني", + "view": "عرض", + "fromUserName": "من", + "sentDate": "تاريخ الإرسال", + "itemTypeDisplayName": "اسم العرض", + "none": "بدون", + "createNewChat": "إنشاء محادثة جديدة", + "brainMarathon": "ماراثون الدماغ", + "contestTopicAbout": "سيكون موضوع المسابقة حول:", + "gameDate": "تاريخ اللعبة:", + "gameTime": "وقت اللعب:", + "joinMarathon": "انضم إلى ماراثون", + "joinDemoMarathon": "انضم إلى الماراثون التجريبي", + "demo": "تجريبي", + "minutes": "الدقائق", + "seconds": "ثواني", + "note": "ملحوظة:", + "demoMarathonNoteP1": + "يمكنك لعب ماراثون العرض لتتعلم كيف يعمل. يمكنك الانضمام إلى ماراثون", + "demoMarathonNoteP2": "خمس دقائق", + "demoMarathonNoteP3": "قبل الوقت الفعلي.", + "sponsoredBy": "برعاية:", + "question": "سؤال", + "marathoners": "المشاركون في الماراثون", + "marathoner": "مشارك في الماراثون", + "prize": "جائزة:", + "winnerSelection": "اختيار الفائز", + "qualifiers": "تصفيات", + "qualifier": "المؤهل", + "getReadyForContest": "استعد للمسابقة القادمة:", + "winnerSelectedRandomly": "سيتم اختيار الفائز عشوائيًا من بين المتأهلين.", + "fingersCrossed": "نتمنى لك التوفيق!", + "congrats": "مبروك !!!", + "allQuestionsCorrect": "لقد أجبت على جميع الأسئلة بشكل صحيح.", + "otp": "OTP", + "verification": "تَحَقّق", + "resend": "إعادة إرسال", + "codeExpire": "انتهت صلاحية رمز التحقق", + "typeheretoreply": "اكتب هنا للرد", + "favorite": "مفضلتي", + "searchfromchat": "البحث من الدردشة", + "yourAnswerCorrect": "إجابتك صحيحة", + "youMissedTheQuestion": + "نفد منك الوقت. أنت خارج اللعبة. لكن يمكنك الاستمرار وكمشاهد.", + "wrongAnswer": + "إجابتك غير صحيحة. أنت خارج اللعبة. لكن يمكنك الاستمرار وكمشاهد.", + "oops": "أوه!!!", + "winner": "الفائز", + "youWantToLeaveMarathon": + "هل أنت متأكد أنك تريد العودة؟ سوف تخرج من المسابقة.", + "ourSponsor": "راعينا:", + "startingIn": "يبدأ في", + "youAreOutOfContest": "أنت خارج المسابقة.", + "winners": "الفائزون", + "expireAfter": "تنتهي بعد", + "oneWeek": "أسبوع واحد", + "twoWeek": "أسبوعان", + "noUpcoming": "لا توجد مسابقات قادمة", + "fakeLocation": + "تم رصد محاولة استخدام موقع جغرافي غير حقيقي. يُعد ذلك مخالفة، وقد تم إشعار إدارة الموارد البشرية.", + "noWinner": "لم يفز أحد اليوم.", + "myTeam": "فريقي", + "youCanPlayDemo": "لكن يمكنك لعب العرض", + "group": "مجموعة", + "searchGroup": "البحث عن مجموعة", + "connectHmgWifi": "قم بتوصيل HMG WIFI", + "connectedHmgWifi": "متصل بشبكة HMG Wi-Fi", + "itgForms": "نماذج (ITG)", + "resetAdPassword": "إعادة تعيين كلمة مرور AD", + "myDocuments": "مستنداتي", + "requiredDocuments": "المستندات المطلوبة", + "optionalDocuments": "المستندات الاختيارية", + "allDocuments": "كافة المستندات", + "expiredDocuments": "المستندات منتهية الصلاحية", + "missingDocuments": "مستندات مفقودة", + "uploadedDocuments": "المستندات التي تم تحميلها", + "addAtLeastOneAttachment": "الرجاء إضافة مرفق واحد على الأقل.", + "pleaseClickButtonToJoinMarathon": + "الرجاء الضغط على الزر أدناه للانضمام إلى الماراثون", + "youCannotJoinTheMarathon": + "لا يمكنك الانضمام إلى الماراثون لأنك تجاوزت الحد الزمني", + "open": "فتح", + "paymentRequest": "طلب الدفع", + "paymentDetails": "تفاصيل الدفع", + "requestNo": "رقم الطلب", + "requesterEmpNum": "رقم الموظف الذي قدم الطلب", + "requesterEmpName": "اسم الموظف مقدم الطلب", + "prepareEmpName": "اسم الموظف المُعدّ للطلب", + "requesterPositionName": "اسم الوظيفة لمقدم الطلب", + "preparePositionName": "المسمى الوظيفي لمُعدّ الطلب", + "requesterPayrollName": "اسم مقدم الطلب", + "payingORGName": "اسم الجهة الدافعة", + "requestAmount": "مبلغ الطلب", + "typeofPayment": "نوع الدفع", + "beneficiaryDetails": "تفاصيل المستفيد", + "beneficiaryName": "اسم المستفيد", + "idIqama": "رقم الهوية الوطنية أو الإقامة", + "beneficiaryBankName": "بنك المستفيد", + "sadadNumber": "رقم سداد", + "beneficiaryIBAN": "رقم IBAN للمستفيد", + "purchaseOrders": "أوامر الشراء", + "approvalDate": "تاريخ الموافقة", + "pOAmount": "مبلغ الطلب", + "versionStatus": "حالة الإصدار", + "supplierNo": "رقم المورد", + "general": "عام", + "requesterOperatingUnit": "وحدة تشغيل مقدم الطلب", + "prepareEmpNum": "الرقم الوظيفي لمُعدّ الطلب", + "supplierInfo": "معلومات المورد", + "supplierAcNo": "رقم حساب المورد", + "supplierAcName": "اسم حساب المورد", + "supplierIBAN": "رقم IBAN للمورد", + "supplierCRNo": "رقم السجل التجاري", + "suppliedAcNo": "رقم الحساب المقدم", + "patientRefundInvoice": "فاتورة استرداد مبلغ للمريض", + "patientNumber": "رقم المريض", + "patientName": "اسم المريض", + "invoiceDate": "تاريخ الفاتورة", + "refundInvoice": "فاتورة الاسترداد", + "hospitalClinic": "المستشفى / العيادة", + "graphicalAnalysis": "التحليل البياني", + "itemHistoryAnalysis": "تحليل سجل الصنف", + "pOno": "رقم أمر الشراء", + "oprUnit": "وحدة التشغيل", + "qtyOrdered": "الكمية المطلوبة", + "qtyReceived": "الكمية المستلمة", + "bonusQty": "الكمية المجانية", + "balQty": "الكمية المتبقية", + "videoCall": "مكالمة فيديو", + "addUsers": "إضافة مستخدمين إلى المجموعة", + "admin": "مشرف", + "areYouSureWantTodelete": "هل أنت متأكد أنك تريد الحذف؟", + "confirmGrantMazayaAccess": + "هل أنت متأكد أنك تريد منح صلاحية مزايا لـ \"{}\"؟", + "confirmRevokeMazayaAccess": + "هل أنت متأكد أنك تريد إلغاء صلاحية مزايا لـ \"{}\"؟", + "yes": "نعم", + "no": "لا", + "mazayaInviteInstructions": + "يرجى تحميل تطبيق مهمم من الرابط أعلاه واستخدام بيانات الاعتماد التالية لتسجيل الدخول", + "cameraPermissionRequiredToScan": + "يرجى منح صلاحية الكاميرا لمسح رمز الاستجابة السريعة", + "cameraPermissionPermanentlyDenied": + "يرجى تفعيل صلاحية الكاميرا من إعدادات التطبيق > الصلاحيات", + "openSettings": "فتح الإعدادات", + "nationalIdOrIqamaFallback": "رقم الهوية الوطنية أو رقم الإقامة الخاص بك", + "Attendance": "الحضور", + "audioCall": "مكالمة صوتية", + "editGroups": "تعديل المجموعة", + "enterGroupName": "يرجى إدخال اسم المجموعة الصحيح", + "enterGroupNamePlease": "يرجى إدخال اسم المجموعة", + "groupMembers": "أعضاء المجموعة", + "groupName": "اسم المجموعة", + "groupNameshouldbe": "يجب أن يتكون اسم المجموعة من 10 أحرف على الأقل", + "manage": "إدارة", + "manageGroup": "إدارة المجموعة", + "members": "الأعضاء", + "searchByUserName": "البحث بواسطة اسم المستخدم", + "shareScreen": "مشاركة الشاشة", + "start": "بدء", + "about": "عن", + "explore": "استكشاف", + "mazaya": "مزايا", + "benefits": "المزايا", + "mazayaDesc": "اكتشف الخصومات والعروض الخاصة المتاحة للموظفين", + "viewallofferMazaya": "عرض جميع عروض مزايا", + "buyerName": "اسم المشتري", + "buyerNumber": "رقم المشتري", + "highestBidder": "أعلى مزايد", + "remarks": "ملاحظات", + "faHeader": "بيانات رأس طلب الأصول الثابتة", + "bookTypeCode": "رمز نوع الدفتر", + "categoryCode": "رمز الفئة", + "categoryGroup": "مجموعة الفئات", + "faLINES": "تفاصيل الأصول", + "assetNumber": "رقم الأصل", + "assetDescription": "وصف الأصل", + "barCodeNumber": "رقم الباركود", + "datePlaceInService": "تاريخ وضعه في الخدمة", + "serialNumber": "الرقم التسلسلي", + "disposedDate": "تاريخ الاستبعاد", + "netBookValue": "القيمة الدفترية الصافية", + "purchasedPrice": "سعر الشراء", + "usefulLife": "العمر الإنتاجي", + "yearsUsed": "سنوات الاستخدام", + "faRequest": "طلب استبعاد أصل ثابت", + "showMore": "عرض المزيد", + "ticketDetails": "تفاصيل التذكرة", + "currentTicketBalance": "رصيد التذكرة الحالي", + "ticketHistory": "سجل التذاكر", + "proceedBooking": "المتابعة إلى الحجز", + "courses": "الدورات التدريبية", + "course": "دورة", + "status": "الحالة", + "deadline": "الموعد النهائي", + "overdue": "متأخر", + "noCoursesAvailable": "لا توجد دورات متاحة", + "pullToRefresh": "اسحب للتحديث", + "courseUrlNotAvailable": "رابط الدورة غير متوفر", + "etqanOVR": "الإبلاغ عن واقعة", + "etqanRequest": "طلبات الإبلاغ عن الوقائع", + "selectProject": "حدد المشروع", + "project": "مشروع", + "keen": "KEEN", + "reportIncident": "الإبلاغ عن الحادث", + "myIncidents": "حوادثي", + "incidentDetails": "تفاصيل الحادث", + "ticketNumber": "رقم التذكرة", + "ticketId": "معرف التذكرة", + "pleaseSelectProject": "الرجاء اختيار مشروع", + "viewAttachment": "عرض المرفق", + "parkingQr": "رمز QR لمواقف السيارات", + "startNow": "ابدأ الآن", + "destination_type": "نوع الوجهة", + "source_organization_name": "اسم الجهة المرسلة", + "destination_organization_name": "اسم الجهة المستقبلة", + "costCenter": "مركز التكلفة", + "priceSAR": "السعر (ريال سعودي)", + "amountSAR": "المبلغ (ريال سعودي)", + "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": "لا توجد طريقة استخدام محددة.", + "contactInfo": "معلومات التواصل", + "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": "عرض جميع الفروع ({})", + "searchBranches": "البحث عن الفروع أو المدن...", + "allCities": "الكل", + "allCitiesOption": "جميع المدن", + "noBranchesFound": "لم يتم العثور على فروع مطابقة", + "browseAllBranches": "تصفح جميع الفروع ({}) في مختلف المدن", + "branchesCount": "{} فرع", + "kmAway": "على بعد {} كم", + "dependentDetails": "تفاصيل التابع", + "keyDetails": "البيانات الأساسية", + "verified": "تم التحقق", + "idEnding": "رقم الهوية المنتهي بـ {}", + "servicesAndActions": "الخدمات والإجراءات", + "startDependentFlowsDesc": "بدء إجراءات التابع وإدارة الوصول من مكان واحد.", + "mazayaAccess": "الوصول إلى مزايا", + "accessOff": "معطل", + "allowDependentMazayaOffersDesc": + "السماح لـ {} باستخدام عروض مزايا بعد التحقق من هويته.", + "loginMethod": "طريقة تسجيل الدخول", + "nationalIdAndDob": "الهوية الوطنية وتاريخ الميلاد", + "sponsorOtp": "رمز التحقق المرسل إلى الموظف المسؤول عن التابع", + "revokingPreventsAccessDesc": + "إلغاء الوصول يمنع التابع من استخدام المزايا حتى تعيد السماح به مرة أخرى.", + "locationPermissionRequiredForAttendance": + "يجب منح إذن الموقع لتسجيل الحضور", + "locationServicesRequiredForAttendance": + "يجب تفعيل خدمة الموقع لتسجيل الحضور", + "pleaseProvideLocationPermission": "الرجاء منح إذن الموقع", + "microphoneAccessRequired": + "يحتاج التطبيق إلى إذن الوصول إلى الميكروفون لتسجيل الصوت.", + "shareAccessGuide": "مشاركة دليل الوصول", + "nationalIdOrIqamaNumber": "رقم الهوية الوطنية أو الإقامة", + "multipleCities": "مدن متعددة", + "filters": "الفلاتر", + "availability": "التوفر", + "chooseOneOrBoth": "اختر أحد الخيارين أو كليهما", + "sortBy": "الترتيب حسب", + "recommended": "الموصى بها", + "nearest": "الأقرب", + "highestDiscount": "الأعلى خصمًا", + "newest": "الأحدث", + "distance": "المسافة", + "fromYourCurrentLocation": "من موقعك الحالي", + "any": "الكل", + "minimumDiscount": "أقل نسبة خصم", + "reset": "إعادة تعيين", + "applyFilters": "تطبيق الفلاتر", + "distanceKm": "{} كم", + "discountPercentPlus": "{}%+", + "visitMerchantWebsite": "زيارة موقع التاجر", + "favoriteAddedSuccess": "تمت إضافة التاجر إلى المفضلة بنجاح", + "favoriteRemovedSuccess": "تمت إزالة التاجر من المفضلة بنجاح", + "favoriteError": "تعذّر تحديث المفضلة. يُرجى المحاولة مرة أخرى.", + "merchantWebsiteNotAvailable": "الموقع الإلكتروني للتاجر غير متاح", + "locationServicesRequiredForNearestOffers": + "فعّل خدمات الموقع لعرض العروض القريبة منك", + "locationPermissionRequiredForNearestOffers": + "اسمح للتطبيق بالوصول إلى موقعك لعرض العروض القريبة منك", + "mazayaDeals": "عروض مزايا", + "helloName": "مرحباً، {}", + "findABrandOrSomething": "ابحث عن علامة تجارية أو شيء بسيط", + "exploreYourBenefits": "استكشف مزاياك", + "theMazayaEdit": "اختيارات مزايا", + "exclusivelyYours": "حصرياً لك", + "aLittleExtraJustForMazayaMembers": "مزايا إضافية، خاصة بأعضاء مزايا.", + "seeAll": "عرض الكل", + "latestOnMazaya": "أحدث ما في مزايا", + "goodFoodGreatCompanyEvenBetterPerks": "طعام رائع. رفقة ممتعة. مزايا أفضل.", + "travelEssentials": "أساسيات السفر", + "fromEverydayCareToYourNextAdventure": + "من العناية اليومية إلى مغامرتك القادمة.", + "homeAndKitchen": "المنزل والمطبخ", + "curatedPicksForYourHomeAndKitchen": "اختيارات مميزة لمنزلك ومطبخك.", + "discover": "اكتشف", + "saved": "المحفوظات", + "myFamily": "عائلتي", + "educationAndTraining": "التعليم والتدريب", + "foodsAndDrinks": "الأطعمة والمشروبات", + "travelAndEntertainment": "السفر والترفيه", + "beautyAndFitness": "الجمال واللياقة", + "healthcare": "الرعاية الصحية", + "favoriteMerchant": "العروض المفضلة", + "favoriteOffers": "العروض المفضلة", + "goodToKnow": "معلومات مهمة", + "goodToKnowDesc": + "احتفظ بعضوية مزايا في متناول يدك. تحقق من توفر التاجر والأنشطة المؤهلة والشروط الكاملة قبل الحجز.", + "termsAndConditions": "الشروط والأحكام", + "haveAQuestionAboutOffer": "هل لديك سؤال حول هذا العرض؟", + "getHelp": "الحصول على المساعدة", + "redeemOffer": "استرداد العرض", + "merchantContact": "بيانات التواصل مع التاجر", + "website": "الموقع الإلكتروني", + "phone": "الهاتف", + "viewAll": "عرض الكل", + "moreCitiesCount": "+ {} مدن أخرى", + "findAnEscapeOrAnExperience": "ابحث عن تجربة أو عطلة", + "allOffers": "جميع العروض", + "sampleAvailability": "حسب التوفر", + "offersCount": "{} عروض", + "mazayaExclusive": "حصري لمزايا", + "youAreAllCaughtUp": "لقد شاهدت كل العروض. المزيد قريباً.", + "selectCity": "اختر المدينة", + "searchCities": "البحث عن المدن", + "buyerDetails": "تفاصيل المشتري", + "chatBot": "المساعد الذكي", + "helloAskMe": "مرحباً، اسألني", + "anything": "عن أي شيء...", + "thinking": "جاري التفكير", + "waitingForResponse": "في انتظار الرد...", + "typeAMessage": "اكتب رسالة...", + "initializingChatPleaseWait": "يرجى الانتظار، جاري تهيئة المحادثة...", + "am": "ص", + "pm": "م" + }; + static const Map _en_US = { + "mohemm": "Mohemm", + "english": "English", + "arabic": "عربي", + "login": "Login", + "pleaseEnterLoginDetails": "Please enter the detail below to login", + "username": "Username", + "password": "Password", + "welcomeBack": "Welcome back", + "wouldYouLikeToLoginWithCurrentUsername": + "Would you like to login with current Username?", + "lastLoginDetails": "Last Login Details:", + "verificationType": "Verification Type:", + "pleaseVerify": "Please Verify", + "pleaseVerifyForBio": + "Please verify login with one of the following options", + "verifyThroughFace": "Verify Through Face", + "verifyThroughFingerprint": "Verify Through Fingerprint", + "verifyThroughSMS": "Verify Through SMS", + "verifyThroughWhatsapp": "Verify Through Whatsapp", + "useAnotherAccount": "Use Another Account", + "pleaseEnterTheVerificationCodeSentTo": + "Please enter the verification code sent to ", + "theVerificationCodeWillExpireIn": "The verification code will expire in ", + "goodMorning": "Good Morning", + "markAttendance": "Mark Attendance", + "timeLeftToday": "Time Left Today", + "checkIn": "Check In", + "workList": "Work List", + "leaveBalance": "Leave Balance", + "missingSwipes": "Missing Swipes", + "ticketBalance": "Ticket Balance", + "other": "Other", + "services": "Services", + "viewAllServices": "View All Services", + "monthlyAttendance": "Monthly Attendance", + "vacationRule": "Vacation Rule", + "vacationType": "Vacation Type", + "startDateT": "Start Date", + "endDateT": "End Date", + "workFromHome": "Work From Home", + "ticketRequest": "Ticket Request", + "viewAllOffers": "View All Offers", + "offers": "Offers & ", + "discounts": "Discounts", + "newString": "New", + "setTheNewPassword": "Set the new password", + "typeYourNewPasswordBelow": "Type your new password below", + "typeYourNewActiveDirectoryPasswordBelow": + "Type new active directory password below", + "confirmPassword": "Confirm Password", + "update": "Update", + "title": "Title", + "home": "Home", + "mySalary": "My Salary", + "createRequest": "Create Request", + "forgotPassword": "Forgot Password", + "employeeId": "Employee ID", + "loginCodeWillSentToMobileNumber": + "Please Enter your Employee ID, A login code will be sent to your mobile number", + "changePassword": "Change Password", + "ok": "OK", + "confirm": "Confirm", + "passwordChangedSuccessfully": "Password changed successfully", + "itemsForSale": "Items for Sale", + "attendanceDetails": "Attendance Details", + "order": "order", + "earlyOut": "Early Out", + "shortage": "Shortage", + "excess": "Excess", + "lateIn": "Late In", + "approvedCheckOut": "Approved Check Out", + "approvedCheckIn": "Approved Check In", + "actualCheckOut": "Actual Check Out", + "actualCheckIn": "Actual Check In", + "present": "PRESENT", + "pres": "present", + "shiftTime": "Shift Time", + "absent": "ABSENT", + "attendance": "Attendance", + "scheduleDays": "Schedule\nDays", + "offDays": "Off\nDays", + "nonAnalyzed": "Non\nAnalyzed", + "shortageHour": "Shortage\nHour", + "stats": "Stats", + "completed": "Completed", + "doNotUseRecentPassword": "Do not use recent password", + "atLeastOneLowercase": "At least one lowercase", + "atLeastOneUppercase": "At least one uppercase", + "atLeastOneNumeric": "At least one numeric", + "minimum8Characters": "Minimum 8 characters", + "doNotAddRepeatingLetters": "Do not add repeating letters", + "itShouldContainSpecialCharacter": + "It should not contain special characters", + "confirmPasswordMustMatch": "Confirm password must match", + "sms": "SMS", + "fingerPrint": "Fingerprint", + "face": "Face", + "whatsapp": "Whatsapp", + "reject": "Reject", + "approve": "Approve", + "generate": "Generate", + "cancel": "Cancel", + "requestedItems": "Requested Items", + "request": "Request", + "myRequest": "My Request", + "actions": "Actions", + "delegate": "Delegate", + "request_info": "Request Info", + "attachments": "Attachments", + "info": "Info.", + "employeeNumber": "Employee Number", + "assignmentNumber": "Assignment Number", + "employeeName": "Employee Name", + "scheduleDate": "Schedule Date", + "shiftType": "Shift Type", + "shift": "Shift", + "breakText": "Break", + "actualSwipeStart": "Actual Swipe Start", + "actualSwipeEnd": "Actual Swipe End", + "approvedSwipeStart": "Approved Swipe Start", + "approvedSwipeStartReason": "Approved Swipe Start Reason", + "approvedSwipeEnd": "Approved Swipe End", + "approvedSwipeEndReason": "Approved Swipe End Reason", + "from": "From", + "to": "To", + "sent": "Sent", + "closed": "Closed", + "id": "ID", + "responder": "Responder", + "jobTitle": "Job Title", + "grade": "Grade", + "jobCategory": "Job Category", + "category": "Category", + "employeeEmailAddress": "Employee Email Address", + "payrollBranch": "Payroll Branch", + "yourChangeHasBeenSavedSuccessfully": + "Your change has been saved successfully", + "code": "Code", + "unit": "Unit", + "quantity": "Quantity", + "dateRequired": "Date Required", + "lineStatus": "Line Status", + "statusDate": "Status Date", + "transactionType": "Transaction Type", + "operatingUnit": "Operating Unit", + "organizationCode": "Organization Code", + "organization": "Organization", + "fromSubInventory": "From Sub Inventory", + "fromLocator": "From Locator", + "toSubInventory": "To Sub Inventory", + "toLocator": "To Locator", + "shipToLocator": "Ship To Locator", + "itemHistory": "Item History", + "mfg": "MFG", + "lineType": "Line Type", + "price": "Price", + "lineAmount": "Line Amount", + "lineDiscount": "Line Discount %", + "needByDate": "Need By Date", + "promisedDate": "Promised Date", + "deliverToLocation": "Deliver To Location", + "requisitionNumber": "Requisition Number", + "requester": "Requester", + "subject": "Subject", + "quotationAnalysis": "Quotation Analysis", + "description": "Description", + "pleaseEnterDescription": "Please enter description", + "supplier": "Supplier", + "site": "Site", + "buyer": "Buyer", + "preparer": "Preparer", + "creationDate": "Creation Date", + "shipToLocation": "Ship To Location", + "quotationNumber": "Quotation Number", + "quotationDate": "Quotation Date", + "paymentTerms": "Payment Terms", + "currency": "Currency", + "grossAmount": "Gross Amount", + "discountAmount": "Discount Amount", + "customDuty": "Custom Duty", + "shipHandle": "Ship Handle", + "otherCharges": "Other Charges", + "totalPOAmountWithVAT": "Total PO Amount With VAT", + "totalPOAmountInWords": "Total PO Amount In Words", + "requestNumber": "Request Number", + "uom": "UOM", + "operatingCode": "Operating Code", + "poNumber": "PO Number", + "revision": "Revision", + "quantityOrdered": "Quantity Ordered", + "quantityReceived": "Quantity Received", + "bonusQuantity": "Bonus Quantity", + "purchasePrice": "Purchase Price", + "discountPer": "Discount %", + "balanceQuantity": "Balance Quantity", + "netPrice": "Net Price", + "closureStatus": "Closure Status", + "quotationNetPrice": "Quotation Net Price", + "quotationUOM": "Quotation UOM", + "quotationQty": "Quotation Qty", + "itemCode": "item Code", + "vendorName": "Vendor Name", + "quotationMFGPartNumber": "Quotation MFG Part Number", + "quotationDeliveryDate": "Quotation Delivery Date", + "quotationBonusQuantity": "Quotation Bonus Quantity", + "quotationLineTotal": "Quotation Line Total", + "rfqUOM": "RFQ UOM", + "rfqQty": "RFQ Qty", + "rfqNumber": "RFQ Number", + "msg": "Hello {} in the {} world ", + "msg_named": "{} are written in the {lang} language", + "clickMe": "Click me", + "human": "Human", + "resources": "Resources", + "details": "Details", + "noDataAvailable": "No Data Available", + "productName": "Product Name", + "productDescription": "Product Description", + "unitPrice": "Unit Price", + "manufacturerName": "Manufacturer Name", + "manufacturerPartName": "Manufacturer Part Name", + "supplierName": "Supplier Name", + "supplierContact": "Supplier Contact", + "chargeToPatient": "Charge To Patient", + "justification": "Justification", + "itemDescription": "Item Description", + "groupCode": "Group Code", + "primaryUOM": "Primary UOM", + "subgroupDescription": "Subgroup Description", + "subgroupCode": "Subgroup Code", + "groupDescription": "Group Description", + "templateName": "Template Name", + "itemCreationStatus": "Item Creation Status", + "standardizationApprovalStatus": "Standardization Approval Status", + "standardizationApprovalRejectionReason": + "Standardization Approval Rejection Reason", + "analyzedBy": "Analyzed By", + "approvedDate": "Approved Date", + "itemType": "Item Type", + "relatedTo": "Related To", + "requestDate": "Request Date", + "analyzedDate": "Analyzed Date", + "urgent": "Urgent", + "requestDetails": "Request Details", + "approvalLevel": "Approval Level", + "requesterDetails": "Requester Details", + "myAttendance": "My Attendance", + "workOnBreak": "Work On Break", + "next": "Next", + "apply": "Apply", + "mobile": "Mobile", + "year": "Year", + "month": "Month", + "day": "Day", + "completingYear": "We appreciate you for completing the service of", "address": "Address", - "contactDetails": "Contact Details", - "familyDetails": "Family Members", - "effectiveDate": "Effective Date", - "country": "Country" - }, - "clicked": { - "zero": "You clicked {} times!", - "one": "You clicked {} time!", - "two": "You clicked {} times!", - "few": "You clicked {} times!", - "many": "You clicked {} times!", - "other": "You clicked {} times!" - }, - "gender": { - "male": "Hi man ;) ", - "female": "Hello girl :)", - "with_arg": { - "male": "Hi man ;) {}", - "female": "Hello girl :) {}" - } - }, - "reset_locale": "Reset Language", - "chat": "Chat", - "mychats": "My Chats", - "createNewChat": "Create New Chat", - "brainMarathon": "Brain Marathon", - "contestTopicAbout": "Contest Topic will be about:", - "gameDate": "Game Date:", - "gameTime": "Game Time:", - "joinMarathon": "Join Marathon", - "joinDemoMarathon": "Join Demo Marathon", - "demo": "Demo", - "minutes": "Minutes", - "seconds": "Seconds", - "note": "Note:", - "demoMarathonNoteP1": "You can play the demo Marathon to learn how it works. You can join the Marathon", - "demoMarathonNoteP2": "5 Minutes", - "demoMarathonNoteP3": "before the actual time.", - "sponsoredBy": "Sponsored By:", - "question": "Question", - "marathoners": "Marathoners", - "marathoner": "Marathoner", - "prize": "Prize:", - "advancedSearch": "Advanced Search", - "openNot": "Open Notifications", - "fyi": "FYI Notifications", - "toDo": "To Do Notifications", - "all": "All Notifications", - "meNot": "Notifications from Me", - "view": "View", - "fromUserName": "From User Name", - "sentDate": "Sent Date", - "itemTypeDisplayName": "Item Type Display Name", - "none": "None", - "winnerSelection": "Winner Selection", - "qualifiers": "Qualifiers", - "qualifier": "Qualifier", - "getReadyForContest": "Get Ready for the coming contest:", - "winnerSelectedRandomly": "The winner will be selected randomly among the qualifiers.", - "fingersCrossed": "Fingers Crossed!!!", - "congrats": "Congratulations!!!", - "otp": "OTP", - "verification": "Verification", - "resend": "Resend", - "codeExpire": "The verification code has been expired", - "allQuestionsCorrect": "You have answered all questions correct", - "typeheretoreply": "Type here to reply", - "favorite": "My Favorites", - "searchfromchat": "Search from chat", - "yourAnswerCorrect": "Your answer is correct", - "youMissedTheQuestion": "You ran out of time. You are out of the game. But you can continue as a viewer.", - "wrongAnswer": "Your answer is Incorrect. You are out of the game. But you can continue as a viewer.", - "oops": "Ooopsss!!!!", - "winner": "WINNER", - "youWantToLeaveMarathon": "Are you sure you want to go back? You will be out of the contest.", - "ourSponsor": "Our Sponsor:", - "startingIn": "Starting in", - "youAreOutOfContest": "You are out of the contest.", - "winners": "WINNERS!!!", - "expireAfter": "Expires After", - "oneWeek": "1 Week", - "twoWeek": "2 Week", - "noUpcoming": "There is no upcoming", - "fakeLocation": "We traced out that you try to use a fake location! This is considered a violation, and HR has been notified.", - "noWinner": "Sad! No one won today.", - "myTeam": "My Team", - "youCanPlayDemo": "But you can play demo", - "group": "Groups", - "searchGroup": "Search Group", - "connectHmgWifi": "Connect HMG WIFI", - "connectedHmgWifi": "Connected HMG WIFI", - "itgForms": "ITG Forms", - "resetAdPassword": "Reset AD Password", - "myDocuments": "My Documents", - "requiredDocuments": "Required Documents", - "optionalDocuments": "Optional Documents", - "allDocuments": "All\nDocuments", - "expiredDocuments": "Expired\nDocuments", - "missingDocuments": "Missing\nDocuments", - "uploadedDocuments": "Uploaded\nDocuments", - "manage": "Manage", - "members": "Members", - "areYouSureWantTodelete": "Are you sure want to delete?", - "groupMembers": "Group Members", - "manageGroup": "Manage Group", - "admin": "Admin", - "addUsers": "Add users to the group", - "editGroups": "Edit Group", - "groupNameshouldbe": "Group name should be minimum 10 character long", - "enterGroupName": "Please enter valid group Name", - "groupName": "Group Name", - "enterGroupNamePlease": "Please enter group name", - "audioCall": "Audio Call", - "videoCall": "Video Call", - "shareScreen": "Share Screen", - "searchByUserName": "Search By Username", - "userSearch": "User Search", - "userName": "User Name", - "userId": "UserID", - "open": "open", - "addAtLeastOneAttachment": "Please add at least one attachment.", - "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", - "paymentRequest": "Pay Request", - "paymentDetails": "Payment Details", - "requestNo": "Request No", - "requesterEmpNum": "Requester Employee Num", - "requesterEmpName": "Requester Employee Name", - "prepareEmpName": "Prepare Employee Name", - "requesterPositionName": "Requester Position Name", - "preparePositionName": "Prepare Position Name", - "requesterPayrollName": "Requester Payroll Name", - "payingORGName": "Paying Organization Name", - "requestAmount": "Request Amount", - "typeofPayment": "Type of Payment", - "beneficiaryDetails": "Beneficiary Details", - "beneficiaryName": "Beneficiary Name", - "idIqama": "Saudi ID / Iqama", - "beneficiaryBankName": "Beneficiary Bank Name", - "sadadNumber": "Sadad Number", - "beneficiaryIBAN": "Beneficiary IBAN", - "purchaseOrders": "Purchase Orders", - "approvalDate": "Approval Date", - "pOAmount": "PO Amount", - "versionStatus": "Version Status", - "supplierNo": "Supplier No", - "general": "General", - "prepareEmpNum": "Prepare Employee Num", - "supplierInfo": "Supplier Information", - "supplierAcNo": "Supplier Account No", - "supplierAcName": "Supplier Account Name", - "supplierIBAN": "Supplier IBAN", - "supplierCRNo": "CR Number", - "suppliedAcNo": "Supplied Account No.", - "patientRefundInvoice": "Patient Refund Invoice", - "patientNumber": "Patient Number", - "patientName": "Patient Name", - "invoiceDate": "Invoice Date", - "refundInvoice": "Refund Invoice", - "hospitalClinic": "Hospital Clinic", - "graphicalAnalysis": "Graphical Analysis", - "itemHistoryAnalysis": "Item History Analysis", - "pOno": "P.O #", - "oprUnit": "Opr. Unit", - "qtyOrdered": "Qty. Ordered", - "qtyReceived": "Qty. Received", - "bonusQty": "Bonus Qty.", - "balQty": "Bal. Qty.", - "start": "Start", - "about": "About", - "explore": "Explore", - "mazaya": "MAZAYA", - "benefits": "Benefits", - "mazayaDesc": "Discover special Discounts and offers available to Employees", - "viewallofferMazaya": "View All Offers", - "buyerName": "Buyer Name", - "buyerNumber": "Buyer Number", - "highestBidder": "Highest Bidder", - "remarks": "Remarks", - "faHeader": "FA Header Details", - "bookTypeCode": "Book Type Code", - "categoryCode": "Category Code", - "categoryGroup": "Category Group", - "faLINES": "Asset Details", - "assetNumber": "Asset Number", - "assetDescription": "Asset Description", - "barCodeNumber": "BarCode Number", - "datePlaceInService": "Date place In Service", - "serialNumber": "Serial Number", - "disposedDate": "Disposed Date", - "netBookValue": "Net Book Value", - "purchasedPrice": "Purchased Price", - "usefulLife": "Useful Life", - "yearsUsed": "Years Used", - "faRequest": "FA Disposal Request", - "showMore": "Show More", - "ticketDetails": "Ticket Details", - "currentTicketBalance": "Current Ticket Balance", - "ticketHistory": "Ticket History", - "proceedBooking": "Proceed For Booking", - "courses": "Courses", - "course": "Course", - "status": "Status", - "deadline": "Deadline", - "overdue": "Overdue", - "noCoursesAvailable": "No Courses Available", - "pullToRefresh": "Pull to refresh", - "courseUrlNotAvailable": "Course URL not available", - "etqanOVR": "Report An Incident", - "etqanRequest": "Incident Requests", - "selectProject": "Select Project", - "project": "Project", - "keen": "Keen", - "reportIncident": "Report Incident", - "myIncidents": "My Incidents", - "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", - "destination_organization_name": "Destination Organization Name", - "costCenter": "Cost Center", - "priceSAR": "Price (SAR)", - "amountSAR": "Amount (SAR)", - "amuLastThreeMonths": "AMU (Last 3 months)", - "buyerDetails": "Buyer Details", - "chatBot": "ChatBot", - "helloAskMe": "Hello, ask me", - "anything": "anything...", - "thinking": "Thinking", - "waitingForResponse": "Waiting for response...", - "typeAMessage": "Type a message..", - "initializingChatPleaseWait": "Please wait, initializing chat...", - "am": "AM", - "pm": "PM" -}; -static const Map> mapLocales = {"ar_SA": _ar_SA, "en_US": _en_US}; + "phoneNumber": "Phone Number", + "businessGroup": "Business", + "Payroll": "Payroll", + "civilIdentityNumber": "Civil Identity Number", + "dateOfBirth": "Date of Birth", + "maritalStatus ": "Marital Status ", + "fullName": "Full Name", + "remove": "Remove", + "Attendance": "Attendance", + "submit": "Submit", + "submitAsAnonymous": "Submit as Anonymous", + "areYouSureYouWantToSubmit": "Are you sure you want to submit?", + "comments": "Comments", + "writeComment": "Write a comment", + "approversList": "Approvers List", + "yourRequestHasBeenSubmittedForApprovals": + "Your request has been submitted for approvals", + "monthlyPaySlip": "Monthly Pay Slip", + "particular": "Particular", + "earnings": "Earnings", + "deductions": "Deductions", + "paymentMethodName": "Payment Method Name", + "bankName": "Bank Name", + "branchCode": "Branch Code", + "accountNo": "Account No", + "summaryOfInformation": "Summary of Information", + "totalPayAmount": "Total Pay Amount", + "paymentInformation": "Payment Information", + "amount": "Amount", + "correctCurrentDatails": "correct or complete the current details", + "selectType": "Select the type of change you want to make", + "enterNewInfo": + "Enter new Information because of a real change to the current details (e.g because of a change in marital status)", + "endDate": "*End Date", + "removeThisMember": "Are You Sure You Want to Remove this Member?", + "wantUpdateThisMember": "Are You Sure You Want to Update this Member?", + "addNewFamilyMember": "Add New Family Member", + "addRow": "Add new row", + "pleaseSelect": "Please Select *", + "delete": "delete", + "add": "Add", + "edit": "Edit", + "myProfile": "My Profile", + "mowadhafhi": "Mowadhafi", + "searchAnnouncements": "Search Announcements", + "announcements": "Announcements", + "swipeRequest": "Swipe Request", + "serviceType": "Service Type", + "departmentName": "Department Name", + "selectDepartment": "Select Department", + "relatedSection": "Related Section", + "selectSection": "Select Section", + "relatedTopic": "Related Topic", + "selectTopic": "Select Topic", + "supportingDocument": "Supporting Document", + "mowadhafhiRequest": "Mowadhafi Request", + "ticketReference": "Ticket Reference", + "section": "Section", + "topic": "Topic", + "actionBy": "Action By", + "pending": "Pending", + "pendingTransactions": "Pending Transactions", + "selectRequestType": "Please select request type", + "dateFrom": "Date From", + "dateTo": "Date To", + "requestName": "Request Name", + "createdFor": "Created For", + "requestType": "Request Type", + "requestCreatedSuccessfully": "Request created successfully", + "search": "Search", + "filterEnterPrDetailsHere": "Enter PR Details here", + "filterEnterPoDetailsHere": "Enter PO Details here", + "wantToReject": "Are you sure want to reject?", + "employeeDigitalID": "Employee Digital ID", + "businessCard": "Business Card", + "checkOut": "Check Out", + "regular": "Regular", + "mark": "Mark", + "performance": "Performance Evaluation", + "performanceEvaluationIn": "Your performance Evaluation in", + "valuationIn": "Performance Evaluation in", + "viewBusinessCard": "View Business Card", + "performanceEvaluation": "Performance Evaluation", + "logout": "Logout", + "selectMethodOfAttendance": "Select the method to mark the attendance", + "comeNearHMGWifi": "Please come near to HMG wifi", + "deliverNotificationToMeRegardless": + "Deliver notifications to me regardless of any general rules", + "close": "Close", + "respond": "Respond", + "vacationRuleAdded": "Vacation rule added", + "selectTypeT": "Select Type", + "notification": "Notification", + "selectNotification": "Select Notification", + "ifAllSelectedYouWillSkip": "*If All is selected, you will skip to step 3", + "applyForVacationRule": "Apply for Vacation Rule", + "step1": "Step 1", + "step2": "Step 2", + "step3": "Step 3", + "message": "Message", + "writeAMessage": "Write a message", + "notificationReassign": "Notification Reassign", + "selectEmployee": "Select Employee", + "searchEmployeeForReplacement": "Search employee for replacement", + "searchForEmployee": "Search for Employee", + "pleaseSpecifyEndTime": "Please specify End Time", + "pleaseSelectNotificationReassign": "Please select notification reassign", + "pleaseSelectEmployeeForReplacement": + "Please select employee for replacement", + "pleaseSelectAction": "Please select action", + "pleaseSelectDate": "Please select date", + "todayAttendance": "Today's Attendance", + "viewAttendance": "View Attendance", + "teamMembers": "Team Members", + "profileDetails": "Profile Details", + "noResultsFound": "No Results Found", + "searchBy": "Search by", + "myTeamMembers": "My Team Members", + "save": "Save", + "TurnNotificationsFor": "Turn on notifications for", + "worklistSettings": "Worklist Settings", + "absenceType": "Absence Type", + "absenceCategory": "Absence Category", + "days": "Days", + "hours": "Hours", + "approvalStatus": "Approval Status", + "absenceStatus": "Absence Status", + "poweredBy": "Powered By", + "cloudSolutions": "Cloud Solutions", + "subordinateLeave": "Subordinate Leave", + "numberDays": "Number of days", + "selectTemplate": "Select Template", + "myPostedAds": "My posted ads", + "browseCategories": "Browse Categories", + "searchItems": "Search Items", + "offerAndDiscounts": "Offer & Discounts", + "offerValid": "Offer Valid", + "offerExpired": "Offer Expired", + "whatAreYouOffering": "What are you offering?", + "selectCategory": "Select Category", + "inProgress": "InProgress", + "locked": "Locked", + "addDetails": "Add Details", + "reviewAndSell": "Review & Sell", + "itemTitle": "Item Title", + "itemCondition": "Item Condition", + "used": "Used", + "region": "Region", + "selectRegion": "Select Region", + "itemPrice": "Item Price", + "itemPhotos": "Item Photos", + "itemInfo": "Item Info", + "uploadAttachment": "Upload Attachment", + "selectFromGalleryOrOpenCamera": "Select from gallery or open camera", + "openCamera": "Open\nCamera", + "uploadFromGallery": "Upload from\nGallery", + "name": "Name", + "email": "Email", + "noHistoryAvailable": "No History Available", + "purchaseRequisition": "Purchase Requisition", + "moveOrder": "Move Order", + "humanResource": "Human Resource", + "purchaseOrder": "Purchase Order", + "ITGForms": "ITG Forms", + "itemCreation": "Item Creation", + "stamp": "Stamp", + "addFavoriteList": "Do you want to add {name} in your favorite list", + "feedbackUserExperience": + "This is to get the feedback about the user experience", + "rateUI": "How would you like to rate", + "rateUI2": "How do you satisfied with this application", + "submitSurvey": "Submit Survey", + "typeHere": "Type here", + "infoDetail": "Info Detail", + "amount_detail": "Amount Detail", + "currentBalance": "Current Balance", + "currentLeaveBalance": "Current Leave Balance", + "calculatedDays": "Calculated Days", + "totalDays": "Total Days", + "usedBalance": "Used", + "infants": "Infants", + "child": "Child", + "adult": "Adult", + "updateMember": "Are You Sure You Want to Update this Member?", + "fieldIsEmpty": "'{data}' Field is empty. Please select", + "pleaseEnterComments": "Please enter comments", + "skip": "Skip", + "typeCurrentPasswordBelow": "Type Your Current password below", + "currentPassword": "Current password", + "concurrentReports": "Concurrent Reports", + "EnterNewAddressMoved": "Enter a new address if you have moved", + "CorrectAddress": "Correct or amend this address", + "SelectChangeWantToMake": "Select the type of change you want to make", + "profile": { + "reset_password": { + "label": "Reset Password", + "username": "Username", + "password": "password", + }, + "profileCompletionPer": "Profile Completion", + "completeProfile": "Complete Profile", + "personalInformation": "Personal Information", + "basicDetails": "Basic Details", + "address": "Address", + "contactDetails": "Contact Details", + "familyDetails": "Family Members", + "effectiveDate": "Effective Date", + "country": "Country", + }, + "clicked": { + "zero": "You clicked {} times!", + "one": "You clicked {} time!", + "two": "You clicked {} times!", + "few": "You clicked {} times!", + "many": "You clicked {} times!", + "other": "You clicked {} times!", + }, + "gender": { + "male": "Hi man ;) ", + "female": "Hello girl :)", + "with_arg": {"male": "Hi man ;) {}", "female": "Hello girl :) {}"}, + }, + "reset_locale": "Reset Language", + "chat": "Chat", + "mychats": "My Chats", + "createNewChat": "Create New Chat", + "brainMarathon": "Brain Marathon", + "contestTopicAbout": "Contest Topic will be about:", + "gameDate": "Game Date:", + "gameTime": "Game Time:", + "joinMarathon": "Join Marathon", + "joinDemoMarathon": "Join Demo Marathon", + "demo": "Demo", + "minutes": "Minutes", + "seconds": "Seconds", + "note": "Note:", + "demoMarathonNoteP1": + "You can play the demo Marathon to learn how it works. You can join the Marathon", + "demoMarathonNoteP2": "5 Minutes", + "demoMarathonNoteP3": "before the actual time.", + "sponsoredBy": "Sponsored By:", + "question": "Question", + "marathoners": "Marathoners", + "marathoner": "Marathoner", + "prize": "Prize:", + "advancedSearch": "Advanced Search", + "openNot": "Open Notifications", + "fyi": "FYI Notifications", + "toDo": "To Do Notifications", + "all": "All Notifications", + "meNot": "Notifications from Me", + "view": "View", + "fromUserName": "From User Name", + "sentDate": "Sent Date", + "itemTypeDisplayName": "Item Type Display Name", + "none": "None", + "winnerSelection": "Winner Selection", + "qualifiers": "Qualifiers", + "qualifier": "Qualifier", + "getReadyForContest": "Get Ready for the coming contest:", + "winnerSelectedRandomly": + "The winner will be selected randomly among the qualifiers.", + "fingersCrossed": "Fingers Crossed!!!", + "congrats": "Congratulations!!!", + "otp": "OTP", + "verification": "Verification", + "resend": "Resend", + "codeExpire": "The verification code has been expired", + "allQuestionsCorrect": "You have answered all questions correct", + "typeheretoreply": "Type here to reply", + "favorite": "My Favorites", + "searchfromchat": "Search from chat", + "yourAnswerCorrect": "Your answer is correct", + "youMissedTheQuestion": + "You ran out of time. You are out of the game. But you can continue as a viewer.", + "wrongAnswer": + "Your answer is Incorrect. You are out of the game. But you can continue as a viewer.", + "oops": "Ooopsss!!!!", + "winner": "WINNER", + "youWantToLeaveMarathon": + "Are you sure you want to go back? You will be out of the contest.", + "ourSponsor": "Our Sponsor:", + "startingIn": "Starting in", + "youAreOutOfContest": "You are out of the contest.", + "winners": "WINNERS!!!", + "expireAfter": "Expires After", + "oneWeek": "1 Week", + "twoWeek": "2 Week", + "noUpcoming": "There is no upcoming", + "fakeLocation": + "We traced out that you try to use a fake location! This is considered a violation, and HR has been notified.", + "noWinner": "Sad! No one won today.", + "myTeam": "My Team", + "youCanPlayDemo": "But you can play demo", + "group": "Groups", + "searchGroup": "Search Group", + "connectHmgWifi": "Connect HMG WIFI", + "connectedHmgWifi": "Connected HMG WIFI", + "itgForms": "ITG Forms", + "resetAdPassword": "Reset AD Password", + "myDocuments": "My Documents", + "requiredDocuments": "Required Documents", + "optionalDocuments": "Optional Documents", + "allDocuments": "All\nDocuments", + "expiredDocuments": "Expired\nDocuments", + "missingDocuments": "Missing\nDocuments", + "uploadedDocuments": "Uploaded\nDocuments", + "manage": "Manage", + "members": "Members", + "areYouSureWantTodelete": "Are you sure want to delete?", + "confirmGrantMazayaAccess": + "Are you sure you want to grant Mazaya access to \"{}\"?", + "confirmRevokeMazayaAccess": + "Are you sure you want to revoke Mazaya access to \"{}\"?", + "yes": "Yes", + "no": "No", + "mazayaInviteInstructions": + "Please download the MoheM app from the above link and use the following credentials to login", + "cameraPermissionRequiredToScan": + "Please grant camera permission to scan the QR code", + "cameraPermissionPermanentlyDenied": + "Please allow camera permission from the app settings > Permissions", + "openSettings": "Open Settings", + "nationalIdOrIqamaFallback": "Your National ID or Iqama Number", + "groupMembers": "Group Members", + "manageGroup": "Manage Group", + "admin": "Admin", + "addUsers": "Add users to the group", + "editGroups": "Edit Group", + "groupNameshouldbe": "Group name should be minimum 10 character long", + "enterGroupName": "Please enter valid group Name", + "groupName": "Group Name", + "enterGroupNamePlease": "Please enter group name", + "audioCall": "Audio Call", + "videoCall": "Video Call", + "shareScreen": "Share Screen", + "searchByUserName": "Search By Username", + "userSearch": "User Search", + "userName": "User Name", + "userId": "UserID", + "open": "open", + "addAtLeastOneAttachment": "Please add at least one attachment.", + "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", + "paymentRequest": "Pay Request", + "paymentDetails": "Payment Details", + "requestNo": "Request No", + "requesterEmpNum": "Requester Employee Num", + "requesterEmpName": "Requester Employee Name", + "prepareEmpName": "Prepare Employee Name", + "requesterPositionName": "Requester Position Name", + "preparePositionName": "Prepare Position Name", + "requesterPayrollName": "Requester Payroll Name", + "payingORGName": "Paying Organization Name", + "requestAmount": "Request Amount", + "typeofPayment": "Type of Payment", + "beneficiaryDetails": "Beneficiary Details", + "beneficiaryName": "Beneficiary Name", + "idIqama": "Saudi ID / Iqama", + "beneficiaryBankName": "Beneficiary Bank Name", + "sadadNumber": "Sadad Number", + "beneficiaryIBAN": "Beneficiary IBAN", + "purchaseOrders": "Purchase Orders", + "approvalDate": "Approval Date", + "pOAmount": "PO Amount", + "versionStatus": "Version Status", + "supplierNo": "Supplier No", + "general": "General", + "prepareEmpNum": "Prepare Employee Num", + "supplierInfo": "Supplier Information", + "supplierAcNo": "Supplier Account No", + "supplierAcName": "Supplier Account Name", + "supplierIBAN": "Supplier IBAN", + "supplierCRNo": "CR Number", + "suppliedAcNo": "Supplied Account No.", + "patientRefundInvoice": "Patient Refund Invoice", + "patientNumber": "Patient Number", + "patientName": "Patient Name", + "invoiceDate": "Invoice Date", + "refundInvoice": "Refund Invoice", + "hospitalClinic": "Hospital Clinic", + "graphicalAnalysis": "Graphical Analysis", + "itemHistoryAnalysis": "Item History Analysis", + "pOno": "P.O #", + "oprUnit": "Opr. Unit", + "qtyOrdered": "Qty. Ordered", + "qtyReceived": "Qty. Received", + "bonusQty": "Bonus Qty.", + "balQty": "Bal. Qty.", + "start": "Start", + "about": "About", + "explore": "Explore", + "mazaya": "MAZAYA", + "benefits": "Benefits", + "mazayaDesc": + "Discover special Discounts and offers available to Employees", + "viewallofferMazaya": "View All Offers", + "buyerName": "Buyer Name", + "buyerNumber": "Buyer Number", + "highestBidder": "Highest Bidder", + "remarks": "Remarks", + "faHeader": "FA Header Details", + "bookTypeCode": "Book Type Code", + "categoryCode": "Category Code", + "categoryGroup": "Category Group", + "faLINES": "Asset Details", + "assetNumber": "Asset Number", + "assetDescription": "Asset Description", + "barCodeNumber": "BarCode Number", + "datePlaceInService": "Date place In Service", + "serialNumber": "Serial Number", + "disposedDate": "Disposed Date", + "netBookValue": "Net Book Value", + "purchasedPrice": "Purchased Price", + "usefulLife": "Useful Life", + "yearsUsed": "Years Used", + "faRequest": "FA Disposal Request", + "showMore": "Show More", + "ticketDetails": "Ticket Details", + "currentTicketBalance": "Current Ticket Balance", + "ticketHistory": "Ticket History", + "proceedBooking": "Proceed For Booking", + "courses": "Courses", + "course": "Course", + "status": "Status", + "deadline": "Deadline", + "overdue": "Overdue", + "noCoursesAvailable": "No Courses Available", + "pullToRefresh": "Pull to refresh", + "courseUrlNotAvailable": "Course URL not available", + "etqanOVR": "Report An Incident", + "etqanRequest": "Incident Requests", + "selectProject": "Select Project", + "project": "Project", + "keen": "Keen", + "reportIncident": "Report Incident", + "myIncidents": "My Incidents", + "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", + "destination_organization_name": "Destination Organization Name", + "costCenter": "Cost Center", + "priceSAR": "Price (SAR)", + "amountSAR": "Amount (SAR)", + "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", + "mazayaAccessRevokedDesc": + "You are not authorized to access the Mazaya Program.", + "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.", + "contactInfo": "Contact Information", + "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", + "searchBranches": "Search branches or cities...", + "allCities": "All", + "allCitiesOption": "All Cities", + "noBranchesFound": "No branches found", + "browseAllBranches": "Browse all {} branches across cities", + "branchesCount": "{} Branches", + "kmAway": "{} KM Away", + "dependentDetails": "Dependent Details", + "keyDetails": "Key details", + "verified": "Verified", + "idEnding": "ID ending {}", + "servicesAndActions": "Services & actions", + "startDependentFlowsDesc": + "Start dependent flows and manage access from one place.", + "mazayaAccess": "Mazaya access", + "accessOff": "Off", + "allowDependentMazayaOffersDesc": + "Allow {} to use Mazaya offers with her verified identity.", + "loginMethod": "Login method", + "nationalIdAndDob": "National ID & Date of Birth", + "sponsorOtp": "Sponsor OTP", + "revokingPreventsAccessDesc": + "Revoking prevents all future dependent access until you allow it again.", + "locationPermissionRequiredForAttendance": + "You need to give location permission to mark attendance", + "locationServicesRequiredForAttendance": + "You need to enable location services to mark attendance", + "pleaseProvideLocationPermission": "Please provide location permission", + "microphoneAccessRequired": + "The app needs microphone access to be able to record audio.", + "shareAccessGuide": "Share Access Guide", + "nationalIdOrIqamaNumber": "National ID or Iqama Number", + "multipleCities": "Multi-city", + "filters": "Filters", + "availability": "Availability", + "chooseOneOrBoth": "Choose one or both", + "sortBy": "Sort by", + "recommended": "Recommended", + "nearest": "Nearest", + "highestDiscount": "Highest discount", + "newest": "Newest", + "distance": "Distance", + "fromYourCurrentLocation": "From your current location", + "any": "Any", + "minimumDiscount": "Minimum discount", + "reset": "Reset", + "applyFilters": "Apply filters", + "distanceKm": "{} km", + "discountPercentPlus": "{}%+", + "visitMerchantWebsite": "Visit Merchant Website", + "merchantWebsiteNotAvailable": "Merchant website not available", + "locationServicesRequiredForNearestOffers": + "You need to enable location services to show nearest offers", + "locationPermissionRequiredForNearestOffers": + "You need to give location permission to show nearest offers", + "mazayaDeals": "Mazaya Deals", + "helloName": "Hello, {}", + "findABrandOrSomething": "Find a brand or a little something", + "exploreYourBenefits": "Explore your benefits", + "theMazayaEdit": "The Mazaya Edit", + "exclusivelyYours": "Exclusively yours", + "aLittleExtraJustForMazayaMembers": + "A little extra, just for Mazaya members.", + "seeAll": "See all", + "latestOnMazaya": "Latest on Mazaya", + "goodFoodGreatCompanyEvenBetterPerks": + "Good food. Great company. Even better perks.", + "travelEssentials": "Travel Essentials", + "fromEverydayCareToYourNextAdventure": + "From everyday care to your next adventure.", + "homeAndKitchen": "Home & Kitchen", + "curatedPicksForYourHomeAndKitchen": + "Curated picks for your home and kitchen.", + "discover": "Discover", + "saved": "Saved", + "myFamily": "My Family", + "educationAndTraining": "Education & Training", + "foodsAndDrinks": "Foods & Drinks", + "travelAndEntertainment": "Travel & Entertainment", + "beautyAndFitness": "Beauty & Fitness", + "healthcare": "Healthcare", + "favoriteAddedSuccess": "Added to favorites successfully", + "favoriteRemovedSuccess": "Removed from favorites successfully", + "favoriteError": "An error occurred while updating favorites", + "favoriteMerchant": "Favorite Offers", + "favoriteOffers": "Favorite Offers", + "goodToKnow": "Good to know", + "goodToKnowDesc": + "Keep your Mazaya membership handy. Check the merchant's availability, eligible activities and full terms before you book.", + "termsAndConditions": "Terms & conditions", + "haveAQuestionAboutOffer": "Have a question about this offer?", + "getHelp": "Get help", + "redeemOffer": "Redeem offer", + "merchantContact": "Merchant contact", + "website": "Website", + "phone": "Phone", + "viewAll": "View all", + "moreCitiesCount": "+ {} more cities", + "findAnEscapeOrAnExperience": "Find an escape or an experience", + "allOffers": "All offers", + "sampleAvailability": "sample availability", + "offersCount": "{} offers", + "mazayaExclusive": "MAZAYA EXCLUSIVE", + "youAreAllCaughtUp": "You're all caught up. More little extras soon.", + "selectCity": "Select City", + "searchCities": "Search cities", + "buyerDetails": "Buyer Details", + "chatBot": "ChatBot", + "helloAskMe": "Hello, ask me", + "anything": "anything...", + "thinking": "Thinking", + "waitingForResponse": "Waiting for response...", + "typeAMessage": "Type a message..", + "initializingChatPleaseWait": "Please wait, initializing chat...", + "am": "AM", + "pm": "PM" + }; + static const Map> mapLocales = { + "ar_SA": _ar_SA, + "en_US": _en_US, + }; } diff --git a/lib/generated/locale_keys.g.dart b/lib/generated/locale_keys.g.dart index f265901..bb26ed0 100644 --- a/lib/generated/locale_keys.g.dart +++ b/lib/generated/locale_keys.g.dart @@ -2,7 +2,7 @@ // ignore_for_file: constant_identifier_names -abstract class LocaleKeys { +abstract class LocaleKeys { static const mohemm = 'mohemm'; static const english = 'english'; static const arabic = 'arabic'; @@ -14,7 +14,8 @@ abstract class LocaleKeys { static const userSearch = 'userSearch'; static const password = 'password'; static const welcomeBack = 'welcomeBack'; - static const wouldYouLikeToLoginWithCurrentUsername = 'wouldYouLikeToLoginWithCurrentUsername'; + static const wouldYouLikeToLoginWithCurrentUsername = + 'wouldYouLikeToLoginWithCurrentUsername'; static const lastLoginDetails = 'lastLoginDetails'; static const verificationType = 'verificationType'; static const pleaseVerify = 'pleaseVerify'; @@ -24,8 +25,10 @@ abstract class LocaleKeys { static const verifyThroughSMS = 'verifyThroughSMS'; static const verifyThroughWhatsapp = 'verifyThroughWhatsapp'; static const useAnotherAccount = 'useAnotherAccount'; - static const pleaseEnterTheVerificationCodeSentTo = 'pleaseEnterTheVerificationCodeSentTo'; - static const theVerificationCodeWillExpireIn = 'theVerificationCodeWillExpireIn'; + static const pleaseEnterTheVerificationCodeSentTo = + 'pleaseEnterTheVerificationCodeSentTo'; + static const theVerificationCodeWillExpireIn = + 'theVerificationCodeWillExpireIn'; static const goodMorning = 'goodMorning'; static const markAttendance = 'markAttendance'; static const timeLeftToday = 'timeLeftToday'; @@ -49,7 +52,8 @@ abstract class LocaleKeys { static const newString = 'newString'; static const setTheNewPassword = 'setTheNewPassword'; static const typeYourNewPasswordBelow = 'typeYourNewPasswordBelow'; - static const typeYourNewActiveDirectoryPasswordBelow = 'typeYourNewActiveDirectoryPasswordBelow'; + static const typeYourNewActiveDirectoryPasswordBelow = + 'typeYourNewActiveDirectoryPasswordBelow'; static const confirmPassword = 'confirmPassword'; static const update = 'update'; static const title = 'title'; @@ -58,7 +62,8 @@ abstract class LocaleKeys { static const createRequest = 'createRequest'; static const forgotPassword = 'forgotPassword'; static const employeeId = 'employeeId'; - static const loginCodeWillSentToMobileNumber = 'loginCodeWillSentToMobileNumber'; + static const loginCodeWillSentToMobileNumber = + 'loginCodeWillSentToMobileNumber'; static const changePassword = 'changePassword'; static const ok = 'ok'; static const confirm = 'confirm'; @@ -94,7 +99,8 @@ abstract class LocaleKeys { static const atLeastOneNumeric = 'atLeastOneNumeric'; static const minimum8Characters = 'minimum8Characters'; static const doNotAddRepeatingLetters = 'doNotAddRepeatingLetters'; - static const itShouldContainSpecialCharacter = 'itShouldContainSpecialCharacter'; + static const itShouldContainSpecialCharacter = + 'itShouldContainSpecialCharacter'; static const confirmPasswordMustMatch = 'confirmPasswordMustMatch'; static const sms = 'sms'; static const fingerPrint = 'fingerPrint'; @@ -137,7 +143,8 @@ abstract class LocaleKeys { static const category = 'category'; static const employeeEmailAddress = 'employeeEmailAddress'; static const payrollBranch = 'payrollBranch'; - static const yourChangeHasBeenSavedSuccessfully = 'yourChangeHasBeenSavedSuccessfully'; + static const yourChangeHasBeenSavedSuccessfully = + 'yourChangeHasBeenSavedSuccessfully'; static const code = 'code'; static const unit = 'unit'; static const quantity = 'quantity'; @@ -232,7 +239,8 @@ abstract class LocaleKeys { static const templateName = 'templateName'; static const itemCreationStatus = 'itemCreationStatus'; static const standardizationApprovalStatus = 'standardizationApprovalStatus'; - static const standardizationApprovalRejectionReason = 'standardizationApprovalRejectionReason'; + static const standardizationApprovalRejectionReason = + 'standardizationApprovalRejectionReason'; static const analyzedBy = 'analyzedBy'; static const approvedDate = 'approvedDate'; static const itemType = 'itemType'; @@ -258,7 +266,7 @@ abstract class LocaleKeys { static const Payroll = 'Payroll'; static const civilIdentityNumber = 'civilIdentityNumber'; static const dateOfBirth = 'dateOfBirth'; - static const maritalStatus = 'maritalStatus '; + static const maritalStatus = 'maritalStatus '; static const fullName = 'fullName'; static const remove = 'remove'; static const submit = 'submit'; @@ -267,7 +275,8 @@ abstract class LocaleKeys { static const comments = 'comments'; static const writeComment = 'writeComment'; static const approversList = 'approversList'; - static const yourRequestHasBeenSubmittedForApprovals = 'yourRequestHasBeenSubmittedForApprovals'; + static const yourRequestHasBeenSubmittedForApprovals = + 'yourRequestHasBeenSubmittedForApprovals'; static const monthlyPaySlip = 'monthlyPaySlip'; static const particular = 'particular'; static const earnings = 'earnings'; @@ -289,7 +298,7 @@ abstract class LocaleKeys { static const enterNewInfo = 'enterNewInfo'; static const endDate = 'endDate'; static const removeThisMember = 'removeThisMember'; - static const wantUpdateThisMember = 'wantUpdateThisMember '; + static const wantUpdateThisMember = 'wantUpdateThisMember '; static const addNewFamilyMember = 'addNewFamilyMember'; static const addRow = 'addRow'; static const pleaseSelect = 'pleaseSelect'; @@ -336,7 +345,8 @@ abstract class LocaleKeys { static const mark = 'mark'; static const selectMethodOfAttendance = 'selectMethodOfAttendance'; static const comeNearHMGWifi = 'comeNearHMGWifi'; - static const deliverNotificationToMeRegardless = 'deliverNotificationToMeRegardless'; + static const deliverNotificationToMeRegardless = + 'deliverNotificationToMeRegardless'; static const close = 'close'; static const respond = 'respond'; static const vacationRuleAdded = 'vacationRuleAdded'; @@ -355,8 +365,10 @@ abstract class LocaleKeys { static const searchEmployeeForReplacement = 'searchEmployeeForReplacement'; static const searchForEmployee = 'searchForEmployee'; static const pleaseSpecifyEndTime = 'pleaseSpecifyEndTime'; - static const pleaseSelectNotificationReassign = 'pleaseSelectNotificationReassign'; - static const pleaseSelectEmployeeForReplacement = 'pleaseSelectEmployeeForReplacement'; + static const pleaseSelectNotificationReassign = + 'pleaseSelectNotificationReassign'; + static const pleaseSelectEmployeeForReplacement = + 'pleaseSelectEmployeeForReplacement'; static const pleaseSelectAction = 'pleaseSelectAction'; static const pleaseSelectDate = 'pleaseSelectDate'; static const todayAttendance = 'todayAttendance'; @@ -441,8 +453,10 @@ abstract class LocaleKeys { static const CorrectAddress = 'CorrectAddress'; static const SelectChangeWantToMake = 'SelectChangeWantToMake'; static const profile_reset_password_label = 'profile.reset_password.label'; - static const profile_reset_password_username = 'profile.reset_password.username'; - static const profile_reset_password_password = 'profile.reset_password.password'; + static const profile_reset_password_username = + 'profile.reset_password.username'; + static const profile_reset_password_password = + 'profile.reset_password.password'; static const profile_reset_password = 'profile.reset_password'; static const profile_profileCompletionPer = 'profile.profileCompletionPer'; static const profile_completeProfile = 'profile.completeProfile'; @@ -537,7 +551,8 @@ abstract class LocaleKeys { static const missingDocuments = 'missingDocuments'; static const uploadedDocuments = 'uploadedDocuments'; static const addAtLeastOneAttachment = 'addAtLeastOneAttachment'; - static const pleaseClickButtonToJoinMarathon = 'pleaseClickButtonToJoinMarathon'; + static const pleaseClickButtonToJoinMarathon = + 'pleaseClickButtonToJoinMarathon'; static const youCannotJoinTheMarathon = 'youCannotJoinTheMarathon'; static const open = 'open'; static const paymentRequest = 'paymentRequest'; @@ -590,6 +605,17 @@ abstract class LocaleKeys { static const addUsers = 'addUsers'; static const admin = 'admin'; static const areYouSureWantTodelete = 'areYouSureWantTodelete'; + static const confirmGrantMazayaAccess = 'confirmGrantMazayaAccess'; + static const confirmRevokeMazayaAccess = 'confirmRevokeMazayaAccess'; + static const yes = 'yes'; + static const no = 'no'; + static const mazayaInviteInstructions = 'mazayaInviteInstructions'; + static const cameraPermissionRequiredToScan = + 'cameraPermissionRequiredToScan'; + static const cameraPermissionPermanentlyDenied = + 'cameraPermissionPermanentlyDenied'; + static const openSettings = 'openSettings'; + static const nationalIdOrIqamaFallback = 'nationalIdOrIqamaFallback'; static const Attendance = 'Attendance'; static const audioCall = 'audioCall'; static const editGroups = 'editGroups'; @@ -665,6 +691,204 @@ abstract class LocaleKeys { static const priceSAR = 'priceSAR'; static const amountSAR = 'amountSAR'; static const amuLastThreeMonths = 'amuLastThreeMonths'; + static const mazayaTitle = 'mazayaTitle'; + static const mazayaAccessGranted = 'mazayaAccessGranted'; + static const mazayaAccessGrantedDesc = 'mazayaAccessGrantedDesc'; + static const accessIsNowActive = 'accessIsNowActive'; + static const accessIsNowActiveDesc = 'accessIsNowActiveDesc'; + static const dependentAccessActive = 'dependentAccessActive'; + static const shareableAppDownloadLink = 'shareableAppDownloadLink'; + static const shareableAppDownloadLinkDesc = 'shareableAppDownloadLinkDesc'; + static const downloadLink = 'downloadLink'; + static const shareDownloadLink = 'shareDownloadLink'; + static const copyLink = 'copyLink'; + static const done = 'done'; + static const linkCopied = 'linkCopied'; + static const mazayaAccessRevoked = 'mazayaAccessRevoked'; + static const mazayaAccessRevokedDesc = 'mazayaAccessRevokedDesc'; + static const contactYourSponsor = 'contactYourSponsor'; + static const contactYourSponsorDesc = 'contactYourSponsorDesc'; + static const dependentAccessBlocked = 'dependentAccessBlocked'; + static const returnToSignIn = 'returnToSignIn'; + static const allowMazayaAccess = 'allowMazayaAccess'; + static const revokeMazayaAccess = 'revokeMazayaAccess'; + static const active = 'active'; + static const noAccess = 'noAccess'; + static const offerDetails = 'offerDetails'; + static const offerIdNotAvailable = 'offerIdNotAvailable'; + static const unableToRedeemOffer = 'unableToRedeemOffer'; + static const errorRedeemingOffer = 'errorRedeemingOffer'; + static const useOffer = 'useOffer'; + static const online = 'online'; + static const inStore = 'inStore'; + static const ksa = 'ksa'; + static const riyadh = 'riyadh'; + static const noInstructionsAvailable = 'noInstructionsAvailable'; + static const contactInfo = 'contactInfo'; + static const allCategories = 'allCategories'; + static const noOffersAvailable = 'noOffersAvailable'; + static const addDependents = 'addDependents'; + static const myDependents = 'myDependents'; + static const offDiscount = 'offDiscount'; + static const usageLabel = 'usageLabel'; + static const areYouSureWantToLogOut = 'areYouSureWantToLogOut'; + static const searchDependents = 'searchDependents'; + static const searchFamilyMembers = 'searchFamilyMembers'; + static const addAnotherUser = 'addAnotherUser'; + static const joinMazaya = 'joinMazaya'; + static const joinMazayaSubtitle = 'joinMazayaSubtitle'; + static const continueAsEmployee = 'continueAsEmployee'; + static const continueAsEmployeeSubtitle = 'continueAsEmployeeSubtitle'; + static const pleaseEnterNationalId = 'pleaseEnterNationalId'; + static const pleaseSelectDob = 'pleaseSelectDob'; + static const unableToRetrieveDependentInfo = 'unableToRetrieveDependentInfo'; + static const failedToSendOtp = 'failedToSendOtp'; + static const verificationCodeResent = 'verificationCodeResent'; + static const nationalId = 'nationalId'; + static const addDependent = 'addDependent'; + static const stepVerify = 'stepVerify'; + static const stepBasic = 'stepBasic'; + static const stepAdditional = 'stepAdditional'; + static const findYourDependent = 'findYourDependent'; + static const findYourDependentDesc = 'findYourDependentDesc'; + static const enter10DigitNationalId = 'enter10DigitNationalId'; + static const nationalIdMustBe10Digits = 'nationalIdMustBe10Digits'; + static const dateFormatPlaceholder = 'dateFormatPlaceholder'; + static const searchUser = 'searchUser'; + static const dependentNotFoundNhic = 'dependentNotFoundNhic'; + static const basicInfo = 'basicInfo'; + static const basicInfoDesc = 'basicInfoDesc'; + static const additionalInfo = 'additionalInfo'; + static const additionalInfoDesc = 'additionalInfoDesc'; + static const select = 'select'; + static const enterFieldPlaceholder = 'enterFieldPlaceholder'; + static const requiredField = 'requiredField'; + static const pleaseFillField = 'pleaseFillField'; + static const reviewAndSubmit = 'reviewAndSubmit'; + static const reviewAndSubmitDesc = 'reviewAndSubmitDesc'; + static const arabicName = 'arabicName'; + static const relationship = 'relationship'; + static const sponsor = 'sponsor'; + static const dependentSponsorDetail = 'dependentSponsorDetail'; + static const insuranceEligibility = 'insuranceEligibility'; + static const relationshipStartDate = 'relationshipStartDate'; + static const dependentAddedSuccessfully = 'dependentAddedSuccessfully'; + static const genderMale = 'genderMale'; + static const genderFemale = 'genderFemale'; + static const religionIslam = 'religionIslam'; + static const back = 'back'; + static const submitRequest = 'submitRequest'; + static const enterDependentDetailsToContinue = + 'enterDependentDetailsToContinue'; + static const enterBasicDetailsDesc = 'enterBasicDetailsDesc'; + static const locations = 'locations'; + static const availableOffers = 'availableOffers'; + static const selectOfferToRedeem = 'selectOfferToRedeem'; + static const viewOnMap = 'viewOnMap'; + static const showAllLocations = 'showAllLocations'; + static const searchBranches = 'searchBranches'; + static const allCities = 'allCities'; + static const allCitiesOption = 'allCitiesOption'; + static const noBranchesFound = 'noBranchesFound'; + static const browseAllBranches = 'browseAllBranches'; + static const branchesCount = 'branchesCount'; + static const kmAway = 'kmAway'; + static const dependentDetails = 'dependentDetails'; + static const keyDetails = 'keyDetails'; + static const verified = 'verified'; + static const idEnding = 'idEnding'; + static const servicesAndActions = 'servicesAndActions'; + static const startDependentFlowsDesc = 'startDependentFlowsDesc'; + static const mazayaAccess = 'mazayaAccess'; + static const accessOff = 'accessOff'; + static const allowDependentMazayaOffersDesc = + 'allowDependentMazayaOffersDesc'; + static const loginMethod = 'loginMethod'; + static const nationalIdAndDob = 'nationalIdAndDob'; + static const sponsorOtp = 'sponsorOtp'; + static const revokingPreventsAccessDesc = 'revokingPreventsAccessDesc'; + static const locationPermissionRequiredForAttendance = + 'locationPermissionRequiredForAttendance'; + static const locationServicesRequiredForAttendance = + 'locationServicesRequiredForAttendance'; + static const pleaseProvideLocationPermission = + 'pleaseProvideLocationPermission'; + static const microphoneAccessRequired = 'microphoneAccessRequired'; + static const shareAccessGuide = 'shareAccessGuide'; + static const nationalIdOrIqamaNumber = 'nationalIdOrIqamaNumber'; + static const multipleCities = 'multipleCities'; + static const filters = 'filters'; + static const availability = 'availability'; + static const chooseOneOrBoth = 'chooseOneOrBoth'; + static const sortBy = 'sortBy'; + static const recommended = 'recommended'; + static const nearest = 'nearest'; + static const highestDiscount = 'highestDiscount'; + static const newest = 'newest'; + static const distance = 'distance'; + static const fromYourCurrentLocation = 'fromYourCurrentLocation'; + static const any = 'any'; + static const minimumDiscount = 'minimumDiscount'; + static const reset = 'reset'; + static const applyFilters = 'applyFilters'; + static const distanceKm = 'distanceKm'; + static const discountPercentPlus = 'discountPercentPlus'; + static const visitMerchantWebsite = 'visitMerchantWebsite'; + static const favoriteAddedSuccess = 'favoriteAddedSuccess'; + static const favoriteRemovedSuccess = 'favoriteRemovedSuccess'; + static const favoriteError = 'favoriteError'; + static const merchantWebsiteNotAvailable = 'merchantWebsiteNotAvailable'; + static const locationServicesRequiredForNearestOffers = + 'locationServicesRequiredForNearestOffers'; + static const locationPermissionRequiredForNearestOffers = + 'locationPermissionRequiredForNearestOffers'; + static const mazayaDeals = 'mazayaDeals'; + static const helloName = 'helloName'; + static const findABrandOrSomething = 'findABrandOrSomething'; + static const exploreYourBenefits = 'exploreYourBenefits'; + static const theMazayaEdit = 'theMazayaEdit'; + static const exclusivelyYours = 'exclusivelyYours'; + static const aLittleExtraJustForMazayaMembers = + 'aLittleExtraJustForMazayaMembers'; + static const seeAll = 'seeAll'; + static const latestOnMazaya = 'latestOnMazaya'; + static const goodFoodGreatCompanyEvenBetterPerks = + 'goodFoodGreatCompanyEvenBetterPerks'; + static const travelEssentials = 'travelEssentials'; + static const fromEverydayCareToYourNextAdventure = + 'fromEverydayCareToYourNextAdventure'; + static const homeAndKitchen = 'homeAndKitchen'; + static const curatedPicksForYourHomeAndKitchen = + 'curatedPicksForYourHomeAndKitchen'; + static const discover = 'discover'; + static const saved = 'saved'; + static const myFamily = 'myFamily'; + static const educationAndTraining = 'educationAndTraining'; + static const foodsAndDrinks = 'foodsAndDrinks'; + static const travelAndEntertainment = 'travelAndEntertainment'; + static const beautyAndFitness = 'beautyAndFitness'; + static const healthcare = 'healthcare'; + static const favoriteMerchant = 'favoriteMerchant'; + static const favoriteOffers = 'favoriteOffers'; + static const goodToKnow = 'goodToKnow'; + static const goodToKnowDesc = 'goodToKnowDesc'; + static const termsAndConditions = 'termsAndConditions'; + static const haveAQuestionAboutOffer = 'haveAQuestionAboutOffer'; + static const getHelp = 'getHelp'; + static const redeemOffer = 'redeemOffer'; + static const merchantContact = 'merchantContact'; + static const website = 'website'; + static const phone = 'phone'; + static const viewAll = 'viewAll'; + static const moreCitiesCount = 'moreCitiesCount'; + static const findAnEscapeOrAnExperience = 'findAnEscapeOrAnExperience'; + static const allOffers = 'allOffers'; + static const sampleAvailability = 'sampleAvailability'; + static const offersCount = 'offersCount'; + static const mazayaExclusive = 'mazayaExclusive'; + static const youAreAllCaughtUp = 'youAreAllCaughtUp'; + static const selectCity = 'selectCity'; + static const searchCities = 'searchCities'; static const chatBot = 'chatBot'; static const helloAskMe = 'helloAskMe'; static const anything = 'anything'; diff --git a/lib/models/dependent/get_mobile_login_info_dependent_model.dart b/lib/models/dependent/get_mobile_login_info_dependent_model.dart new file mode 100644 index 0000000..6ab4114 --- /dev/null +++ b/lib/models/dependent/get_mobile_login_info_dependent_model.dart @@ -0,0 +1,92 @@ +class GetMobileLoginInfoDependentModel { + int? iD; + String? dependentID; + String? depedentName; + String? dob; + int? channelID; + int? companyID; + String? deviceType; + String? deviceToken; + int? language; + int? gender; + int? loginType; + String? createdOn; + String? editedOn; + bool? businessCardPrivilege; + + GetMobileLoginInfoDependentModel({ + this.iD, + this.dependentID, + this.depedentName, + this.dob, + this.channelID, + this.companyID, + this.deviceType, + this.deviceToken, + this.language, + this.gender, + this.loginType, + this.createdOn, + this.editedOn, + this.businessCardPrivilege, + }); + + // Convenience aliases for existing logic + String? get nationalId => dependentID; + String? get birthDate => dob; + String? get employeeName => depedentName; + String? get dependentName => depedentName; + + GetMobileLoginInfoDependentModel.fromJson(Map json) { + iD = json['ID']; + dependentID = (json['DependentID'] ?? + json['DEPENDENT_ID'] ?? + json['P_DEP_NATIONAL_ID'] ?? + json['NationalID'] ?? + json['NationalId'] ?? + json['EmployeeID'] ?? + json['UserName']) + ?.toString(); + depedentName = (json['DepedentName'] ?? + json['DependentName'] ?? + json['P_DEPENDENT_NAME'] ?? + json['EmployeeName']) + ?.toString(); + dob = (json['DOB'] ?? + json['BirthDate'] ?? + json['DateOfBirth'] ?? + json['P_DEP_BIRTH_DATE'] ?? + json['P_BIRTH_DATE'] ?? + json['Birth_Date']) + ?.toString(); + channelID = json['ChannelID'] != null ? int.tryParse(json['ChannelID'].toString()) : null; + companyID = json['CompanyID'] != null ? int.tryParse(json['CompanyID'].toString()) : null; + deviceType = json['DeviceType']?.toString(); + deviceToken = json['DeviceToken']?.toString(); + language = json['Language'] != null ? int.tryParse(json['Language'].toString()) : null; + gender = json['Gender'] != null ? int.tryParse(json['Gender'].toString()) : null; + loginType = json['LoginType'] != null ? int.tryParse(json['LoginType'].toString()) : null; + createdOn = json['CreatedOn']?.toString(); + editedOn = json['EditedOn']?.toString(); + businessCardPrivilege = json['BusinessCardPrivilege']; + } + + Map toJson() { + Map data = {}; + data['ID'] = iD; + data['DependentID'] = dependentID; + data['DepedentName'] = depedentName; + data['DOB'] = dob; + data['ChannelID'] = channelID; + data['CompanyID'] = companyID; + data['DeviceType'] = deviceType; + data['DeviceToken'] = deviceToken; + data['Language'] = language; + data['Gender'] = gender; + data['LoginType'] = loginType; + data['CreatedOn'] = createdOn; + data['EditedOn'] = editedOn; + data['BusinessCardPrivilege'] = businessCardPrivilege; + return data; + } +} diff --git a/lib/models/generic_response_model.dart b/lib/models/generic_response_model.dart index 1c1662b..540d3d8 100644 --- a/lib/models/generic_response_model.dart +++ b/lib/models/generic_response_model.dart @@ -107,6 +107,7 @@ import 'package:mohem_flutter_app/models/vacation_rule/respond_attributes_list_m import 'package:mohem_flutter_app/models/vacation_rule/vr_item_types_list_model.dart'; import 'package:mohem_flutter_app/models/vacation_rule/wf_look_up_list_model.dart'; import 'package:mohem_flutter_app/models/validate_eit_transaction_list_model.dart'; +import 'package:mohem_flutter_app/models/offers_and_discounts/wala_plus_category_model.dart'; import 'package:mohem_flutter_app/models/worklist/GetRFCEmployeeList.dart'; import 'package:mohem_flutter_app/models/worklist/get_fad_notifications.dart'; import 'package:mohem_flutter_app/models/worklist/get_favorite_replacements_model.dart'; @@ -403,6 +404,14 @@ class GenericResponseModel { AtlasContinueChatResponseDetails? atlasContinueChatResponseDetails; + GetWalaplusDepInfoModel? getWalaplusDepInfoModel; + dynamic getWalaplusDepInfoModelInternal; + List? walaPlusCategories; + String? wpFavoritesAddRemoveMessage; + + String? businessCardQROutside; + EmpBusinessCardInfoModel? empBusinessCardInfoModel; + bool? isEmpLastLoginToDevice; GenericResponseModel({ this.date, this.languageID, @@ -675,6 +684,13 @@ class GenericResponseModel { this.isSMSSent, this.getFADisposalNtfDetails, this.Mohemm_ENSAT_GetItgToken_Response, + this.getWalaplusDepInfoModel, + this.getWalaplusDepInfoModelInternal, + this.walaPlusCategories, + this.wpFavoritesAddRemoveMessage, + this.businessCardQROutside, + this.empBusinessCardInfoModel, + this.isEmpLastLoginToDevice, this.atlasInitiateChatResponse, this.atlasContinueChatResponseDetails, @@ -686,7 +702,7 @@ class GenericResponseModel { serviceName = json['ServiceName']; time = json['Time']; androidLink = json['AndroidLink']; - authenticationTokenID = json['AuthenticationTokenID']; + authenticationTokenID = json['AuthenticationTokenID']?.toString(); data = json['Data']; dataw = json['Dataw']; dietType = json['DietType']; @@ -1476,6 +1492,21 @@ class GenericResponseModel { Mohemm_ENSAT_GetItgToken_Response = json['Mohemm_ENSAT_GetItgToken_Response']; + + getWalaplusDepInfoModel = json['GET_WALAPLUS_DEP_INFO_Model'] != null ? GetWalaplusDepInfoModel.fromJson(json['GET_WALAPLUS_DEP_INFO_Model']) : null; + getWalaplusDepInfoModelInternal = json['GET_WALAPLUS_DEP_INFO_Model_Internal']; + + businessCardQROutside = json['BusinessCardQR_Outside']; + empBusinessCardInfoModel = json['GET_EMP_BUSINESS_CARD_INFO_Model'] != null ? EmpBusinessCardInfoModel.fromJson(json['GET_EMP_BUSINESS_CARD_INFO_Model']) : null; + + if (json['WalaPlus_Categories'] != null) { + walaPlusCategories = []; + json['WalaPlus_Categories'].forEach((v) { + walaPlusCategories!.add(WalaPlusCategoryModel.fromJson(v)); + }); + } + wpFavoritesAddRemoveMessage = json['WP_Favorites_AddRemove_Message'] ?? json['wp_Favorites_AddRemove_Message']; + isEmpLastLoginToDevice = json["IsEmpLastLoginToDevice"]; } Map toJson() { @@ -1965,6 +1996,24 @@ class GenericResponseModel { data["Atlas_ContinueChat_ResponseDetails"] = atlasContinueChatResponseDetails?.toJson(); data['Mohemm_ENSAT_GetItgToken_Response'] = this.Mohemm_ENSAT_GetItgToken_Response; + + data['GET_WALAPLUS_DEP_INFO_Model_Internal'] = this.getWalaplusDepInfoModelInternal; + if (this.getWalaplusDepInfoModel != null) { + data['GET_WALAPLUS_DEP_INFO_Model'] = this.getWalaplusDepInfoModel!.toJson(); + } + + data['BusinessCardQR_Outside'] = this.businessCardQROutside; + if (this.empBusinessCardInfoModel != null) { + data['GET_EMP_BUSINESS_CARD_INFO_Model'] = this.empBusinessCardInfoModel!.toJson(); + } + + if (walaPlusCategories != null) { + data['WalaPlus_Categories'] = walaPlusCategories!.map((WalaPlusCategoryModel v) => v.toJson()).toList(); + } + if (this.wpFavoritesAddRemoveMessage != null) { + data['WP_Favorites_AddRemove_Message'] = this.wpFavoritesAddRemoveMessage; + } + data["IsEmpLastLoginToDevice"] = this.isEmpLastLoginToDevice; return data; } } @@ -1991,6 +2040,50 @@ class TicketBookingResult { TicketBookingResult(this.success, this.clientId); } +class GetWalaplusDepInfoModel { + String? pEmpEmailAddress; + String? pEmpMobileNumber; + String? pDependentName; + + GetWalaplusDepInfoModel({this.pEmpEmailAddress, this.pEmpMobileNumber, this.pDependentName}); + + factory GetWalaplusDepInfoModel.fromJson(Map json) => + GetWalaplusDepInfoModel(pEmpEmailAddress: json["P_EMP_EMAIL_ADDRESS"], pEmpMobileNumber: json["P_EMP_MOBILE_NUMBER"], pDependentName: json["P_DEPENDENT_NAME"]); + + Map toJson() => {"P_EMP_EMAIL_ADDRESS": pEmpEmailAddress, "P_EMP_MOBILE_NUMBER": pEmpMobileNumber, "P_DEPENDENT_NAME": pDependentName}; +} + +class EmpBusinessCardInfoModel { + String? pEmailAddress; + String? pFirstName; + String? pLastName; + String? pMobilePhoneNumber; + String? pPositionTitle; + String? pWorkExtensionNumber; + + EmpBusinessCardInfoModel({this.pEmailAddress, this.pFirstName, this.pLastName, this.pMobilePhoneNumber, this.pPositionTitle, this.pWorkExtensionNumber}); + + String get fullName => [pFirstName, pLastName].where((e) => e != null && e.trim().isNotEmpty).join(" "); + + factory EmpBusinessCardInfoModel.fromJson(Map json) => EmpBusinessCardInfoModel( + pEmailAddress: json["P_EMAIL_ADDRESS"], + pFirstName: json["P_FIRST_NAME"], + pLastName: json["P_LAST_NAME"], + pMobilePhoneNumber: json["P_MOBILE_PHONE_NUMBER"], + pPositionTitle: json["P_POSITION_TITLE"], + pWorkExtensionNumber: json["P_WORK_EXTENSION_NUMBER"], + ); + + Map toJson() => { + "P_EMAIL_ADDRESS": pEmailAddress, + "P_FIRST_NAME": pFirstName, + "P_LAST_NAME": pLastName, + "P_MOBILE_PHONE_NUMBER": pMobilePhoneNumber, + "P_POSITION_TITLE": pPositionTitle, + "P_WORK_EXTENSION_NUMBER": pWorkExtensionNumber, + }; +} + class AtlasInitiateChatResponse { String? atlasAccessToken; String? atlasChatSessionId; diff --git a/lib/models/get_employee_contacts.model.dart b/lib/models/get_employee_contacts.model.dart index df0a41f..070e802 100644 --- a/lib/models/get_employee_contacts.model.dart +++ b/lib/models/get_employee_contacts.model.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; class GetEmployeeContactsList { String? cONTACTNAME; @@ -8,16 +9,34 @@ class GetEmployeeContactsList { int? pERSONID; String? pRIMARYCONTACTFLAG; String? rELATIONSHIP; + String? mAZAYAACCESSFLG; + dynamic mazayaAccess; + String? nATIONALIDENTIFIER; - GetEmployeeContactsList( - {this.cONTACTNAME, - this.cONTACTPERSONID, - this.cONTACTRELATIONSHIPID, - this.cONTACTTYPE, - this.dATEOFBIRTH, - this.pERSONID, - this.pRIMARYCONTACTFLAG, - this.rELATIONSHIP}); + // Real ERP response fields - needed to call the grant-access API later (as P_BIRTH_DATE / + // P_NHIC_VERIFIED_FLAG request params). PatientIdentificationID/PatientOutSA are NOT + // response fields - they're constructed at call time (from nATIONALIDENTIFIER, and a + // hardcoded 0, respectively), so they don't belong on this model. + // Not a plain string per ERP - keep untyped so whatever shape comes back (epoch, nested + // date object, etc.) survives round-trip, instead of assuming a format. + dynamic pBIRTHDATE; + String? pNHICVERIFIEDFLAG; + + GetEmployeeContactsList({ + this.cONTACTNAME, + this.cONTACTPERSONID, + this.cONTACTRELATIONSHIPID, + this.cONTACTTYPE, + this.dATEOFBIRTH, + this.pERSONID, + this.pRIMARYCONTACTFLAG, + this.rELATIONSHIP, + this.mAZAYAACCESSFLG, + this.mazayaAccess, + this.nATIONALIDENTIFIER, + this.pBIRTHDATE, + this.pNHICVERIFIEDFLAG, + }); GetEmployeeContactsList.fromJson(Map json) { cONTACTNAME = json['CONTACT_NAME']; @@ -28,18 +47,41 @@ class GetEmployeeContactsList { pERSONID = json['PERSON_ID']; pRIMARYCONTACTFLAG = json['PRIMARY_CONTACT_FLAG']; rELATIONSHIP = json['RELATIONSHIP']; + mAZAYAACCESSFLG = json['MAZAYA_ACCESS_FLG']?.toString() ?? json['MAZAYA_ACCESS']?.toString(); + mazayaAccess = mAZAYAACCESSFLG; + nATIONALIDENTIFIER = + json['NATIONAL_IDENTIFIER']?.toString() ?? + json['NATIONAL_ID']?.toString() ?? + json['CIVIL_ID']?.toString() ?? + json['IQAMA_NUMBER']?.toString() ?? + json['CONTACT_NATIONAL_IDENTIFIER']?.toString() ?? + json['CONTACT_NATIONAL_ID']?.toString() ?? + json['CONTACT_IDENTIFICATION_ID']?.toString() ?? + json['IDENTIFICATION_ID']?.toString() ?? + json['NATIONALID']?.toString(); + if (nATIONALIDENTIFIER == null || nATIONALIDENTIFIER!.trim().isEmpty) { + debugPrint("GetEmployeeContactsList: no national id match found, raw contact json: $json"); + } + + pBIRTHDATE = json['P_BIRTH_DATE']; + pNHICVERIFIEDFLAG = json['P_NHIC_VERIFIED_FLAG']?.toString(); } Map toJson() { - Map data = new Map(); - data['CONTACT_NAME'] = this.cONTACTNAME; - data['CONTACT_PERSON_ID'] = this.cONTACTPERSONID; - data['CONTACT_RELATIONSHIP_ID'] = this.cONTACTRELATIONSHIPID; - data['CONTACT_TYPE'] = this.cONTACTTYPE; - data['DATE_OF_BIRTH'] = this.dATEOFBIRTH; - data['PERSON_ID'] = this.pERSONID; - data['PRIMARY_CONTACT_FLAG'] = this.pRIMARYCONTACTFLAG; - data['RELATIONSHIP'] = this.rELATIONSHIP; + Map data = {}; + data['CONTACT_NAME'] = cONTACTNAME; + data['CONTACT_PERSON_ID'] = cONTACTPERSONID; + data['CONTACT_RELATIONSHIP_ID'] = cONTACTRELATIONSHIPID; + data['CONTACT_TYPE'] = cONTACTTYPE; + data['DATE_OF_BIRTH'] = dATEOFBIRTH; + data['PERSON_ID'] = pERSONID; + data['PRIMARY_CONTACT_FLAG'] = pRIMARYCONTACTFLAG; + data['RELATIONSHIP'] = rELATIONSHIP; + data['MAZAYA_ACCESS_FLG'] = mAZAYAACCESSFLG; + data['MAZAYA_ACCESS'] = mazayaAccess; + data['NATIONAL_IDENTIFIER'] = nATIONALIDENTIFIER; + data['P_BIRTH_DATE'] = pBIRTHDATE; + data['P_NHIC_VERIFIED_FLAG'] = pNHICVERIFIEDFLAG; return data; } -} \ No newline at end of file +} diff --git a/lib/models/get_mobile_login_info_list_model.dart b/lib/models/get_mobile_login_info_list_model.dart index d544ef5..00267a2 100644 --- a/lib/models/get_mobile_login_info_list_model.dart +++ b/lib/models/get_mobile_login_info_list_model.dart @@ -1,6 +1,6 @@ class GetMobileLoginInfoListModel { int? iD; - String? employeeID; + dynamic? employeeID; //do not change this int? channelID; int? companyID; String? deviceType; diff --git a/lib/models/offers_and_discounts/wala_plus_category_model.dart b/lib/models/offers_and_discounts/wala_plus_category_model.dart new file mode 100644 index 0000000..0bd04fe --- /dev/null +++ b/lib/models/offers_and_discounts/wala_plus_category_model.dart @@ -0,0 +1,75 @@ +class WalaPlusCategoryModel { + String? id; + String? image; + String? imageThumb; + String? name; + String? parentId; + + String? countryNameAr; + String? countryNameEn; + + WalaPlusCategoryModel({ + this.id, + this.image, + this.imageThumb, + this.name, + this.parentId, + this.countryNameAr, + this.countryNameEn, + }) { + if ((image == null || image!.isEmpty) && id != null && id!.trim().isNotEmpty && id != "all" && id != "0") { + String cleanId = id!.trim(); + if (cleanId.toLowerCase().endsWith('.png')) { + cleanId = cleanId.substring(0, cleanId.length - 4); + } + image = Uri.encodeFull("https://epharmacy.hmg.com/middleware/images/category/$cleanId.png"); + } + } + + WalaPlusCategoryModel.fromJson(Map json) { + id = (json['id'] ?? json['Id'] ?? json['ID'])?.toString(); + name = (json['name'] ?? json['Name'] ?? json['category_name'] ?? json['categoryName'] ?? json['cat_name'] ?? json['CategoryName'])?.toString(); + parentId = (json['parent_id'] ?? json['Parent_Id'] ?? json['parentId'])?.toString(); + imageThumb = (json['image_thumb'] ?? json['Image_Thumb'] ?? json['imageThumb'])?.toString(); + countryNameAr = (json['country_name_ar'] ?? json['countryNameAr'] ?? json['category_name_ar'] ?? json['categoryNameAr'] ?? json['cat_name_ar'] ?? json['name_ar'])?.toString(); + countryNameEn = (json['country_name_en'] ?? json['countryNameEn'] ?? json['category_name_en'] ?? json['categoryNameEn'] ?? json['cat_name_en'] ?? json['name_en'])?.toString(); + + // Use category image URL from HMG middleware using ID from WalaPlus_Categories API: + // https://epharmacy.hmg.com/middleware/images/category/{id}.png + if (id != null && id!.trim().isNotEmpty && id != "all" && id != "0") { + String cleanId = id!.trim(); + if (cleanId.toLowerCase().endsWith('.png')) { + cleanId = cleanId.substring(0, cleanId.length - 4); + } + image = Uri.encodeFull("https://epharmacy.hmg.com/middleware/images/category/$cleanId.png"); + } else { + image = (json['image'] ?? json['Image'])?.toString(); + } + } + + Map toJson() { + Map data = {}; + data['id'] = id; + data['image'] = image; + data['image_thumb'] = imageThumb; + data['name'] = name; + data['parent_id'] = parentId; + data['country_name_ar'] = countryNameAr; + data['country_name_en'] = countryNameEn; + return data; + } + + bool get isMainCategory => parentId == null || parentId!.isEmpty; + bool get isSvg => (image ?? '').toLowerCase().contains('.svg'); + + String get categoryImageUrl { + if (id != null && id!.trim().isNotEmpty && id != "all" && id != "0") { + String cleanId = id!.trim(); + if (cleanId.toLowerCase().endsWith('.png')) { + cleanId = cleanId.substring(0, cleanId.length - 4); + } + return Uri.encodeFull("https://epharmacy.hmg.com/middleware/images/category/$cleanId.png"); + } + return image ?? ''; + } +} diff --git a/lib/models/offers_and_discounts/wala_plus_city_model.dart b/lib/models/offers_and_discounts/wala_plus_city_model.dart new file mode 100644 index 0000000..e8d6468 --- /dev/null +++ b/lib/models/offers_and_discounts/wala_plus_city_model.dart @@ -0,0 +1,51 @@ +class WalaPlusCityModel { + int? cityId; + String? cityNameAr; + String? cityNameEn; + String? countryNameAr; + String? countryNameEn; + + WalaPlusCityModel({ + this.cityId, + this.cityNameAr, + this.cityNameEn, + this.countryNameAr, + this.countryNameEn, + }); + + factory WalaPlusCityModel.fromJson(Map json) { + return WalaPlusCityModel( + cityId: + json['city_id'] is int + ? json['city_id'] + : int.tryParse(json['city_id']?.toString() ?? ''), + cityNameAr: json['city_name_ar']?.toString(), + cityNameEn: json['city_name_en']?.toString(), + countryNameAr: json['country_name_ar']?.toString(), + countryNameEn: json['country_name_en']?.toString(), + ); + } + + Map toJson() { + return { + 'city_id': cityId, + 'city_name_ar': cityNameAr, + 'city_name_en': cityNameEn, + 'country_name_ar': countryNameAr, + 'country_name_en': countryNameEn, + }; + } + + String getDisplayName(bool isAr) { + String? primary = isAr ? cityNameAr : cityNameEn; + if (primary != null && primary.trim().isNotEmpty) return primary.trim(); + String? fallback = isAr ? cityNameEn : cityNameAr; + return fallback?.trim() ?? ''; + } + + bool get isAllCities { + String en = (cityNameEn ?? '').trim().toLowerCase(); + String ar = (cityNameAr ?? '').trim(); + return en == 'all cities' || ar == 'جميع المدن'; + } +} diff --git a/lib/models/offers_and_discounts/wala_plus_retailer_model.dart b/lib/models/offers_and_discounts/wala_plus_retailer_model.dart new file mode 100644 index 0000000..c124e6b --- /dev/null +++ b/lib/models/offers_and_discounts/wala_plus_retailer_model.dart @@ -0,0 +1,1154 @@ +import 'dart:convert'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:mohem_flutter_app/generated/locale_keys.g.dart'; +import 'package:mohem_flutter_app/models/offers_and_discounts/get_offers_list.dart'; +import 'package:mohem_flutter_app/models/offers_and_discounts/wala_plus_category_model.dart'; + +class WalaPlusRetailersResponse { + int? page; + int? pagesCount; + int? perPage; + List? retailers; + String? countryNameAr; + String? countryNameEn; + + WalaPlusRetailersResponse({this.page, this.pagesCount, this.perPage, this.retailers, this.countryNameAr, this.countryNameEn}); + + factory WalaPlusRetailersResponse.fromJson(Map json) { + List retailersList = []; + dynamic retailersData = json['retailers'] ?? json['Retailers'] ?? json['data'] ?? json['items']; + if (retailersData is String) { + try { + retailersData = jsonDecode(retailersData); + } catch (_) {} + } + if (retailersData is List) { + retailersData.forEach((v) { + if (v is Map) { + retailersList.add(WalaPlusRetailerModel.fromJson(v)); + } else if (v is Map) { + retailersList.add(WalaPlusRetailerModel.fromJson(Map.from(v))); + } + }); + } + + return WalaPlusRetailersResponse( + page: json['page'] is int ? json['page'] : int.tryParse(json['page']?.toString() ?? json['WP_Page']?.toString() ?? '1'), + pagesCount: json['pages_count'] is int ? json['pages_count'] : int.tryParse(json['pages_count']?.toString() ?? json['pagesCount']?.toString() ?? json['totalPages']?.toString() ?? '1'), + perPage: json['per_page'] is int ? json['per_page'] : int.tryParse(json['per_page']?.toString() ?? json['perPage']?.toString() ?? json['WP_Per_page']?.toString() ?? '10'), + retailers: retailersList, + countryNameAr: WalaPlusRetailerModel.decodeUnicodeString(json['country_name_ar'] ?? json['countryNameAr'] ?? json['country_ar']), + countryNameEn: WalaPlusRetailerModel.decodeUnicodeString(json['country_name_en'] ?? json['countryNameEn'] ?? json['country_en']), + ); + } + + Map toJson() { + Map data = {}; + data['page'] = page; + data['pages_count'] = pagesCount; + data['per_page'] = perPage; + data['country_name_ar'] = countryNameAr; + data['country_name_en'] = countryNameEn; + if (retailers != null) { + data['retailers'] = retailers!.map((v) => v.toJson()).toList(); + } + return data; + } +} + +class WalaPlusRetailersByIDResponse { + int? page; + int? pagesCount; + int? perPage; + List? locations; + List? offers; + List? retailer; + + WalaPlusRetailersByIDResponse({this.page, this.pagesCount, this.perPage, this.locations, this.offers, this.retailer}); + + factory WalaPlusRetailersByIDResponse.fromJson(Map json) { + List locList = WalaPlusRetailerModel.parseLocationsList(json['locations'] ?? json['Locations'] ?? json['location'] ?? json['Location']); + List offList = WalaPlusRetailerModel.parseOffersList(json['offers'] ?? json['Offers']); + List catList = []; + dynamic catData = json['categories'] ?? json['Categories'] ?? json['category'] ?? json['Category']; + if (catData is List) { + for (var c in catData) { + if (c is Map) { + catList.add(WalaPlusCategoryModel.fromJson(c)); + } else if (c is Map) { + catList.add(WalaPlusCategoryModel.fromJson(Map.from(c))); + } else if (c is String && c.trim().isNotEmpty) { + catList.add(WalaPlusCategoryModel(name: c.trim())); + } + } + } else if (catData is Map) { + catList.add(WalaPlusCategoryModel.fromJson(Map.from(catData))); + } else if (catData is String && catData.trim().isNotEmpty) { + catList.add(WalaPlusCategoryModel(name: catData.trim())); + } + + List retList = []; + dynamic retData = json['retailer'] ?? json['Retailer']; + if (retData is List) { + for (var r in retData) { + if (r is Map) { + var model = WalaPlusRetailerModel.fromJson(r); + if (model.locations == null || model.locations!.isEmpty) model.locations = locList; + if (model.offers == null || model.offers!.isEmpty) model.offers = offList; + if ((model.categories == null || model.categories!.isEmpty) && catList.isNotEmpty) model.categories = catList; + if ((model.website == null || model.website!.isEmpty || model.website == "null") && (json['website'] != null || json['merchant_website'] != null || json['url'] != null)) { + model.website = (json['website'] ?? json['merchant_website'] ?? json['url'])?.toString(); + } + if (model.isFavorite == null && (json.containsKey('isfavorite') || json.containsKey('is_favorite'))) { + var favVal = json['isfavorite'] ?? json['is_favorite']; + model.isFavorite = favVal == true || favVal == 1 || favVal == 'true' || favVal == '1'; + } + retList.add(model); + } else if (r is Map) { + var model = WalaPlusRetailerModel.fromJson(Map.from(r)); + if (model.locations == null || model.locations!.isEmpty) model.locations = locList; + if (model.offers == null || model.offers!.isEmpty) model.offers = offList; + if ((model.categories == null || model.categories!.isEmpty) && catList.isNotEmpty) model.categories = catList; + if ((model.website == null || model.website!.isEmpty || model.website == "null") && (json['website'] != null || json['merchant_website'] != null || json['url'] != null)) { + model.website = (json['website'] ?? json['merchant_website'] ?? json['url'])?.toString(); + } + if (model.isFavorite == null && (json.containsKey('isfavorite') || json.containsKey('is_favorite'))) { + var favVal = json['isfavorite'] ?? json['is_favorite']; + model.isFavorite = favVal == true || favVal == 1 || favVal == 'true' || favVal == '1'; + } + retList.add(model); + } + } + } else if (retData is Map) { + var model = WalaPlusRetailerModel.fromJson(Map.from(retData)); + if (model.locations == null || model.locations!.isEmpty) model.locations = locList; + if (model.offers == null || model.offers!.isEmpty) model.offers = offList; + if ((model.categories == null || model.categories!.isEmpty) && catList.isNotEmpty) model.categories = catList; + if ((model.website == null || model.website!.isEmpty || model.website == "null") && (json['website'] != null || json['merchant_website'] != null || json['url'] != null)) { + model.website = (json['website'] ?? json['merchant_website'] ?? json['url'])?.toString(); + } + if (model.isFavorite == null && (json.containsKey('isfavorite') || json.containsKey('is_favorite'))) { + var favVal = json['isfavorite'] ?? json['is_favorite']; + model.isFavorite = favVal == true || favVal == 1 || favVal == 'true' || favVal == '1'; + } + retList.add(model); + } + + return WalaPlusRetailersByIDResponse( + page: json['page'] is int ? json['page'] : int.tryParse(json['page']?.toString() ?? json['WP_Page']?.toString() ?? ''), + pagesCount: json['pages_count'] is int ? json['pages_count'] : int.tryParse(json['pages_count']?.toString() ?? json['pagesCount']?.toString() ?? ''), + perPage: json['per_page'] is int ? json['per_page'] : int.tryParse(json['per_page']?.toString() ?? json['perPage']?.toString() ?? ''), + locations: locList, + offers: offList, + retailer: retList, + ); + } + + WalaPlusRetailerModel? get primaryRetailer { + if (retailer != null && retailer!.isNotEmpty) { + return retailer!.first; + } + return null; + } + + Map toJson() { + Map data = {}; + data['page'] = page; + data['pages_count'] = pagesCount; + data['per_page'] = perPage; + if (locations != null) { + data['locations'] = locations!.map((v) => v.toJson()).toList(); + } + if (offers != null) { + data['offers'] = offers!.map((v) => v.toJson()).toList(); + } + if (retailer != null) { + data['retailer'] = retailer!.map((v) => v.toJson()).toList(); + } + return data; + } +} + +class WalaPlusRetailerModel { + String? id; + String? name; + String? offerTitle; + String? offerType; + String? description; + String? info; + String? image; + String? imageThumb; + String? logo; + String? logoThumb; + String? website; + String? type; + dynamic usage; + dynamic weight; + dynamic geoId; + String? pin; + String? barcodeStandard; + String? arHowToUse; + String? enHowToUse; + String? howToUse; + WalaPlusRetailerContact? contact; + dynamic offerWeight; + List? offers; + List? locations; + List? categories; + String? categoryId; + String? categoryName; + String? validity; + String? validTo; + String? validUntil; + String? endDate; + String? expiryDate; + String? duration; + String? email; + String? phone; + String? countryNameAr; + String? countryNameEn; + bool? isFavorite; + + // Drives the "MAZAYA EXCLUSIVE" badge on the retailer card (Figma node 927:496). NOT parsed + // from the WalaPlus_Retailers/WalaPlus_RetailersByID API - this is a different, separate + // "Exclusive" API/section, so callers set this manually on the model after fetching from there. + bool? isExclusive; + + WalaPlusRetailerModel({ + this.id, + this.name, + this.offerTitle, + this.offerType, + this.description, + this.info, + this.image, + this.imageThumb, + this.logo, + this.logoThumb, + this.website, + this.isFavorite, + this.isExclusive, + this.type, + this.usage, + this.weight, + this.geoId, + this.pin, + this.barcodeStandard, + this.arHowToUse, + this.enHowToUse, + this.howToUse, + this.contact, + this.offerWeight, + this.offers, + this.locations, + this.categories, + this.categoryId, + this.categoryName, + this.validity, + this.validTo, + this.validUntil, + this.endDate, + this.expiryDate, + this.duration, + this.email, + this.phone, + this.countryNameAr, + this.countryNameEn, + }); + + static List parseOffersList(data) { + List list = []; + if (data == null) return list; + if (data is String) { + try { + data = jsonDecode(data); + } catch (_) {} + } + if (data is List) { + for (var v in data) { + if (v is Map) { + list.add(WalaPlusOfferModel.fromJson(v)); + } else if (v is Map) { + list.add(WalaPlusOfferModel.fromJson(Map.from(v))); + } + } + } + return list; + } + + static List parseLocationsList(data) { + List list = []; + if (data == null) return list; + if (data is String) { + try { + data = jsonDecode(data); + } catch (_) {} + } + if (data is List) { + for (var v in data) { + if (v is Map) { + list.add(WalaPlusRetailerLocation.fromJson(v)); + } else if (v is Map) { + list.add(WalaPlusRetailerLocation.fromJson(Map.from(v))); + } + } + } + return list; + } + + factory WalaPlusRetailerModel.fromJson(Map json) { + Map targetJson = json; + dynamic offersSource = json['offers'] ?? json['Offers']; + dynamic locationsSource = json['locations'] ?? json['Locations'] ?? json['location'] ?? json['Location']; + + // If json is the wrapper object from WalaPlus_RetailersByID containing 'retailer' key + if (json['retailer'] != null || json['Retailer'] != null) { + dynamic retailerData = json['retailer'] ?? json['Retailer']; + if (retailerData is List && retailerData.isNotEmpty) { + var first = retailerData.first; + if (first is Map) { + targetJson = first; + } else if (first is Map) { + targetJson = Map.from(first); + } + } else if (retailerData is Map) { + targetJson = retailerData; + } else if (retailerData is Map) { + targetJson = Map.from(retailerData); + } + if (locationsSource == null || (locationsSource is List && locationsSource.isEmpty)) { + locationsSource = targetJson['locations'] ?? targetJson['Locations'] ?? targetJson['location'] ?? targetJson['Location']; + } + if (offersSource == null || (offersSource is List && offersSource.isEmpty)) { + offersSource = targetJson['offers'] ?? targetJson['Offers']; + } + } + + List offersList = parseOffersList(offersSource ?? targetJson['offers'] ?? targetJson['Offers']); + List locationsList = parseLocationsList(locationsSource ?? targetJson['locations'] ?? targetJson['Locations'] ?? targetJson['location'] ?? targetJson['Location']); + + List categoriesList = []; + dynamic rawCats = targetJson['categories'] ?? targetJson['Categories'] ?? targetJson['category'] ?? targetJson['Category']; + if (rawCats != null && rawCats is List) { + for (var v in rawCats) { + if (v is Map) { + categoriesList.add(WalaPlusCategoryModel.fromJson(v)); + } else if (v is Map) { + categoriesList.add(WalaPlusCategoryModel.fromJson(Map.from(v))); + } else if (v is String && v.trim().isNotEmpty) { + categoriesList.add(WalaPlusCategoryModel(name: v.trim())); + } + } + } else if (rawCats is Map) { + categoriesList.add(WalaPlusCategoryModel.fromJson(Map.from(rawCats))); + } else if (rawCats is String && rawCats.toString().trim().isNotEmpty && rawCats.toString().trim() != "null") { + categoriesList.add(WalaPlusCategoryModel(name: rawCats.toString().trim())); + } + + String? singleCatName = + (targetJson['category_name'] ?? + targetJson['categoryName'] ?? + targetJson['cat_name'] ?? + targetJson['catName'] ?? + targetJson['CategoryName'] ?? + targetJson['Category_Name'] ?? + targetJson['category_title'] ?? + targetJson['categoryTitle']) + ?.toString() + .trim(); + String? singleCatAr = + (targetJson['category_name_ar'] ?? + targetJson['categoryNameAr'] ?? + targetJson['cat_name_ar'] ?? + targetJson['catNameAr'] ?? + targetJson['category_ar'] ?? + targetJson['country_name_ar'] ?? + targetJson['countryNameAr']) + ?.toString() + .trim(); + String? singleCatEn = + (targetJson['category_name_en'] ?? + targetJson['categoryNameEn'] ?? + targetJson['cat_name_en'] ?? + targetJson['catNameEn'] ?? + targetJson['category_en'] ?? + targetJson['country_name_en'] ?? + targetJson['countryNameEn']) + ?.toString() + .trim(); + String? foundCategoryId = + (targetJson['category_id'] ?? targetJson['categoryId'] ?? targetJson['cat_id'] ?? targetJson['catId'] ?? targetJson['WP_CategoryID'] ?? targetJson['WP_CategoryId'])?.toString().trim(); + + String? rawType = targetJson['type']?.toString().trim(); + String? lowerType = rawType?.toLowerCase(); + bool isTypeStoreKeyword = + lowerType == null || + lowerType.isEmpty || + lowerType == 'online' || + lowerType == 'instore' || + lowerType == 'in_store' || + lowerType == 'in store' || + lowerType == 'store' || + lowerType == 'online,instore' || + lowerType == 'instore,online'; + + if ((singleCatName == null || singleCatName.isEmpty) && !isTypeStoreKeyword && rawType != null && rawType.isNotEmpty) { + singleCatName = rawType; + } + + if (categoriesList.isEmpty) { + if ((singleCatName != null && singleCatName.isNotEmpty && singleCatName != "null") || + (singleCatAr != null && singleCatAr.isNotEmpty && singleCatAr != "null") || + (singleCatEn != null && singleCatEn.isNotEmpty && singleCatEn != "null")) { + categoriesList.add( + WalaPlusCategoryModel(id: foundCategoryId, name: singleCatName ?? singleCatEn ?? singleCatAr, countryNameAr: singleCatAr ?? singleCatName, countryNameEn: singleCatEn ?? singleCatName), + ); + } + } + + WalaPlusRetailerContact? contactObj; + if (targetJson['contact'] != null) { + if (targetJson['contact'] is Map) { + contactObj = WalaPlusRetailerContact.fromJson(Map.from(targetJson['contact'])); + } else if (targetJson['contact'] is String && targetJson['contact'].toString().trim().isNotEmpty) { + contactObj = WalaPlusRetailerContact(website: targetJson['contact'].toString()); + } + } + + String? foundValidity = + targetJson['validity']?.toString() ?? + targetJson['valid_to']?.toString() ?? + targetJson['valid_until']?.toString() ?? + targetJson['end_date']?.toString() ?? + targetJson['expiry_date']?.toString() ?? + targetJson['expire_date']?.toString() ?? + targetJson['duration']?.toString() ?? + targetJson['validity_period']?.toString() ?? + targetJson['valid_duration']?.toString() ?? + targetJson['offer_validity']?.toString(); + + if (foundValidity == null || foundValidity == "null" || foundValidity.trim().isEmpty) { + for (var entry in targetJson.entries) { + String k = entry.key.toLowerCase(); + if ((k.contains('valid') || k.contains('expir') || k.contains('duration')) && !k.contains('isverificationcodevalid') && !k.contains('barcode')) { + if (entry.value != null && entry.value.toString().trim().isNotEmpty && entry.value.toString() != "null") { + foundValidity = entry.value.toString().trim(); + break; + } + } + } + } + + String? foundWebsite = + targetJson['website']?.toString() ?? + targetJson['merchant_website']?.toString() ?? + targetJson['merchantWebsite']?.toString() ?? + targetJson['website_url']?.toString() ?? + targetJson['websiteUrl']?.toString() ?? + targetJson['web_site']?.toString() ?? + targetJson['url']?.toString() ?? + targetJson['merchant_url']?.toString() ?? + targetJson['merchantUrl']?.toString() ?? + targetJson['merchantLink']?.toString() ?? + targetJson['merchant_link']?.toString() ?? + targetJson['link']?.toString() ?? + contactObj?.website; + + if (foundWebsite == null || foundWebsite.trim().isEmpty || foundWebsite == "null") { + if (targetJson['merchant'] is Map) { + var m = targetJson['merchant']; + foundWebsite = (m['website'] ?? m['merchant_website'] ?? m['url'] ?? m['link'])?.toString(); + } + } + + if (foundWebsite == null || foundWebsite.trim().isEmpty || foundWebsite == "null") { + for (var entry in targetJson.entries) { + String k = entry.key.toLowerCase(); + if ((k.contains('website') || k.contains('web_site') || k == 'url' || k.contains('merchant_url')) && !k.contains('image') && !k.contains('logo')) { + if (entry.value != null && entry.value.toString().trim().isNotEmpty && entry.value.toString() != "null") { + foundWebsite = entry.value.toString().trim(); + break; + } + } + } + } + + bool? parsedIsFavorite; + if (targetJson.containsKey('isfavorite')) { + parsedIsFavorite = targetJson['isfavorite'] == true || targetJson['isfavorite'] == 1 || targetJson['isfavorite'] == 'true' || targetJson['isfavorite'] == '1'; + } else if (targetJson.containsKey('is_favorite')) { + parsedIsFavorite = targetJson['is_favorite'] == true || targetJson['is_favorite'] == 1 || targetJson['is_favorite'] == 'true' || targetJson['is_favorite'] == '1'; + } else if (targetJson.containsKey('isFavorite')) { + parsedIsFavorite = targetJson['isFavorite'] == true || targetJson['isFavorite'] == 1 || targetJson['isFavorite'] == 'true' || targetJson['isFavorite'] == '1'; + } else if (targetJson.containsKey('favorite')) { + parsedIsFavorite = targetJson['favorite'] == true || targetJson['favorite'] == 1 || targetJson['favorite'] == 'true' || targetJson['favorite'] == '1'; + } + + return WalaPlusRetailerModel( + id: + targetJson['id']?.toString() ?? + targetJson['WP_RetailerID']?.toString() ?? + targetJson['retailer_id']?.toString() ?? + targetJson['RetailerID']?.toString() ?? + targetJson['retailerId']?.toString(), + name: decodeUnicodeString(targetJson['name']), + offerTitle: decodeUnicodeString(targetJson['offer_title']), + offerType: targetJson['offer_type']?.toString(), + description: decodeUnicodeString(targetJson['description']), + info: decodeUnicodeString(targetJson['info']), + image: targetJson['image']?.toString(), + imageThumb: targetJson['image_thumb']?.toString(), + logo: targetJson['logo']?.toString(), + logoThumb: targetJson['logo_thumb']?.toString(), + website: foundWebsite, + isFavorite: parsedIsFavorite, + type: targetJson['type']?.toString(), + usage: targetJson['usage'] is String ? decodeUnicodeString(targetJson['usage']) : targetJson['usage'], + weight: targetJson['weight'], + geoId: targetJson['geo_id'], + pin: targetJson['pin']?.toString(), + barcodeStandard: targetJson['barcode_standard']?.toString(), + arHowToUse: decodeUnicodeString(targetJson['ar_how_to_use']), + enHowToUse: decodeUnicodeString(targetJson['en_how_to_use']), + howToUse: decodeUnicodeString(targetJson['how_to_use']), + contact: contactObj, + offerWeight: targetJson['offer_weight'], + offers: offersList, + locations: locationsList, + categories: categoriesList, + categoryId: foundCategoryId, + categoryName: singleCatName ?? singleCatEn ?? singleCatAr, + validity: foundValidity, + validTo: targetJson['valid_to']?.toString() ?? targetJson['validTo']?.toString(), + validUntil: targetJson['valid_until']?.toString() ?? targetJson['validUntil']?.toString(), + endDate: targetJson['end_date']?.toString() ?? targetJson['endDate']?.toString(), + expiryDate: targetJson['expiry_date']?.toString() ?? targetJson['expiryDate']?.toString(), + duration: targetJson['duration']?.toString(), + email: targetJson['email']?.toString() ?? targetJson['contact_email']?.toString() ?? contactObj?.primaryEmail, + phone: targetJson['phone']?.toString() ?? targetJson['contact_phone']?.toString() ?? targetJson['mobile']?.toString() ?? contactObj?.primaryPhone, + countryNameAr: decodeUnicodeString(targetJson['country_name_ar'] ?? targetJson['countryNameAr'] ?? targetJson['country_ar']), + countryNameEn: decodeUnicodeString(targetJson['country_name_en'] ?? targetJson['countryNameEn'] ?? targetJson['country_en']), + ); + } + + // Adapter for the Mazaya "Exclusive Offers" section, which reuses the legacy Offers & + // Discounts (ITG) API/implementation (lib/ui/screens/offers_and_discounts) rather than a + // WalaPlus endpoint. Maps whatever overlaps onto this model so the existing grid/card/detail + // screen can render it unchanged - fields with no equivalent on OffersListModel (favorite, + // redeem, locations, contact, ...) are simply left null, and isExclusive hides the actions + // that depend on them (see WalaPlusRetailerCard, RetailerDetailsScreen). + factory WalaPlusRetailerModel.fromExclusiveOffer(OffersListModel offer, {bool isAr = false}) { + String? name = decodeUnicodeString(isAr ? (offer.titleAr ?? offer.titleEn) : (offer.titleEn ?? offer.titleAr)); + String? description = decodeUnicodeString(isAr ? (offer.descriptionAr ?? offer.descriptionEn) : (offer.descriptionEn ?? offer.descriptionAr)); + String? categoryName = decodeUnicodeString(isAr ? (offer.categoryNameAr ?? offer.categoryNameEn) : (offer.categoryNameEn ?? offer.categoryNameAr)); + String? discountText = _formatExclusiveDiscountText(decodeUnicodeString(offer.discountDescription) ?? decodeUnicodeString(offer.discount)); + return WalaPlusRetailerModel( + id: "exclusive-${offer.offersDiscountId ?? ''}", + name: name, + // WalaPlusRetailerCard reads offerTitle as the promo subtitle shown under the name (e.g. + // "20% off") - the discount text is the closest equivalent on a legacy offer. + offerTitle: discountText, + description: description, + info: description, + image: offer.bannerImage, + logo: offer.logo, + website: offer.websiteUrl, + endDate: offer.endDate, + validTo: offer.endDate, + categoryName: categoryName, + categories: categoryName != null ? [WalaPlusCategoryModel(name: categoryName)] : null, + isExclusive: true, + ); + } + + // The legacy Offers & Discounts API sometimes returns a bare number ("10", "20") for its + // discount fields rather than a formatted "10% off" string. WalaPlusRetailerCard's badge + // logic (getDiscountBadge) only recognizes a "%" pattern, so a bare number would + // otherwise render verbatim instead of as a discount. Already-formatted text (containing "%", + // "off", or "خصم") is left untouched. + static String? _formatExclusiveDiscountText(String? raw) { + if (raw == null) return null; + String trimmed = raw.trim(); + if (trimmed.isEmpty) return null; + if (trimmed.contains('%') || trimmed.toLowerCase().contains('off') || trimmed.contains('خصم')) { + return trimmed; + } + if (RegExp(r'^\d+(\.\d+)?$').hasMatch(trimmed)) { + return LocaleKeys.offDiscount.tr(args: ['$trimmed%']); + } + return trimmed; + } + + static String? decodeUnicodeString(Object? val) { + if (val == null) return null; + String s = val.toString().trim(); + if (s.isEmpty || s == "null") return null; + + while ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) { + if (s.length >= 2) { + s = s.substring(1, s.length - 1).trim(); + } else { + break; + } + } + + s = s.replaceAll(r'\/', '/').replaceAll(r'\"', '"').replaceAll(r"\'", "'"); + + s = s.replaceAllMapped(RegExp(r'\\u([0-9a-fA-F]{4})'), (match) { + try { + var codeUnit = int.parse(match.group(1)!, radix: 16); + return String.fromCharCode(codeUnit); + } catch (_) { + return match.group(0)!; + } + }); + + s = s + .replaceAll(r'\u201c', '“') + .replaceAll(r'\u201d', '”') + .replaceAll(r'\u2019', '’') + .replaceAll(r'\u2018', '‘') + .replaceAll(r'\u00a0', ' ') + .replaceAll(r'\u2022', '•') + .replaceAll(r'\u2013', '–') + .replaceAll(r'\u2014', '—') + .replaceAll(r'\u200f', '') + .replaceAll(r'\u200e', '') + .replaceAll(r'\u061f', '؟') + .replaceAll(r'\u201', '“') + .replaceAll('"', '"') + .replaceAll(''', "'") + .replaceAll(''', "'") + .replaceAll(' ', ' ') + .replaceAll('&', '&'); + + return s; + } + + Map toJson() { + Map data = {}; + data['id'] = id; + data['name'] = name; + data['offer_title'] = offerTitle; + data['offer_type'] = offerType; + data['description'] = description; + data['info'] = info; + data['image'] = image; + data['image_thumb'] = imageThumb; + data['logo'] = logo; + data['logo_thumb'] = logoThumb; + data['website'] = website; + data['type'] = type; + data['usage'] = usage; + data['weight'] = weight; + data['geo_id'] = geoId; + data['pin'] = pin; + data['barcode_standard'] = barcodeStandard; + data['ar_how_to_use'] = arHowToUse; + data['en_how_to_use'] = enHowToUse; + data['how_to_use'] = howToUse; + data['contact'] = contact?.toJson(); + data['offer_weight'] = offerWeight; + data['validity'] = validity; + data['valid_to'] = validTo; + data['valid_until'] = validUntil; + data['end_date'] = endDate; + data['expiry_date'] = expiryDate; + data['duration'] = duration; + data['email'] = email; + data['phone'] = phone; + data['country_name_ar'] = countryNameAr; + data['country_name_en'] = countryNameEn; + data['is_favorite'] = isFavorite; + data['isfavorite'] = isFavorite; + data['is_exclusive'] = isExclusive; + if (offers != null) { + data['offers'] = offers!.map((v) => v.toJson()).toList(); + } + if (locations != null) { + data['locations'] = locations!.map((v) => v.toJson()).toList(); + } + if (categories != null) { + data['categories'] = categories!.map((v) => v.toJson()).toList(); + } + data['category_id'] = categoryId; + data['category_name'] = categoryName; + return data; + } + + String? getDisplayCategory({bool isAr = false, List? fallbackCategories}) { + if (categories != null && categories!.isNotEmpty) { + for (var cat in categories!) { + String? primary = isAr ? cat.countryNameAr : cat.countryNameEn; + String? secondary = isAr ? (cat.name ?? cat.countryNameEn) : (cat.name ?? cat.countryNameAr); + String? resolved = + (primary != null && primary.trim().isNotEmpty && primary != "null") ? primary.trim() : (secondary != null && secondary.trim().isNotEmpty && secondary != "null" ? secondary.trim() : null); + if (resolved != null && resolved.isNotEmpty) { + return resolved; + } + } + } + if (categoryName != null && categoryName!.trim().isNotEmpty && categoryName != "null") { + return categoryName!.trim(); + } + if (fallbackCategories != null && categoryId != null && categoryId!.trim().isNotEmpty) { + for (var cat in fallbackCategories) { + if (cat.id?.toString() == categoryId?.toString()) { + String? primary = isAr ? cat.countryNameAr : cat.countryNameEn; + String? secondary = isAr ? (cat.name ?? cat.countryNameEn) : (cat.name ?? cat.countryNameAr); + String? resolved = + (primary != null && primary.trim().isNotEmpty && primary != "null") + ? primary.trim() + : (secondary != null && secondary.trim().isNotEmpty && secondary != "null" ? secondary.trim() : null); + if (resolved != null && resolved.isNotEmpty) { + return resolved; + } + } + } + } + if (type != null && type!.trim().isNotEmpty) { + String lt = type!.trim().toLowerCase(); + if (lt != 'online' && lt != 'instore' && lt != 'store' && lt != 'in_store') { + return type!.trim(); + } + } + return null; + } + + String? get displayImage { + if (image != null && image!.trim().isNotEmpty && image != "null") return image!.trim(); + if (imageThumb != null && imageThumb!.trim().isNotEmpty && imageThumb != "null") return imageThumb!.trim(); + return null; + } + + String? get displayLogo { + if (logo != null && logo!.trim().isNotEmpty && logo != "null") return logo!.trim(); + if (logoThumb != null && logoThumb!.trim().isNotEmpty && logoThumb != "null") return logoThumb!.trim(); + return null; + } + + String? get resolvedWebsite { + if (website != null && website!.trim().isNotEmpty && website != "null") { + return website!.trim(); + } + if (contact?.website != null && contact!.website!.trim().isNotEmpty && contact!.website != "null") { + return contact!.website!.trim(); + } + return null; + } + + bool get hasPhysicalLocation { + if (locations != null && locations!.isNotEmpty) { + for (var loc in locations!) { + if ((loc.cityNameEn != null && loc.cityNameEn!.trim().isNotEmpty && loc.cityNameEn != "null") || + (loc.cityNameAr != null && loc.cityNameAr!.trim().isNotEmpty && loc.cityNameAr != "null") || + (loc.city != null && loc.city!.trim().isNotEmpty && loc.city != "null")) { + return true; + } + } + } + return false; + } + + // Resolves a single location's city using the same 3-pass priority this used to apply + // globally: specific city name first, then a non-generic-country city, then any city at all. + String? _resolveLocationCity(WalaPlusRetailerLocation loc, bool isAr) { + String? cName = isAr ? loc.cityNameAr : loc.cityNameEn; + if (cName != null && cName.trim().isNotEmpty && cName != "null") { + return cName.trim(); + } + if (loc.city != null && loc.city!.trim().isNotEmpty && loc.city != "null") { + String c = loc.city!.trim(); + if (c.toLowerCase() != 'saudi arabia' && c != 'المملكة العربية السعودية') { + return c; + } + } + if (loc.city != null && loc.city!.trim().isNotEmpty && loc.city != "null") { + return loc.city!.trim(); + } + return null; + } + + // Multiple distinct cities across this retailer's locations -> "Multiple Cities" instead of + // just picking the first one found, so the badge doesn't silently hide the other branches. + String getDisplayCity({bool isAr = false}) { + if (locations == null || locations!.isEmpty) return ''; + + Set distinctCities = {}; + for (var loc in locations!) { + String? resolved = _resolveLocationCity(loc, isAr); + if (resolved != null) distinctCities.add(resolved); + } + + if (distinctCities.isEmpty) return ''; + if (distinctCities.length > 1) return LocaleKeys.multipleCities.tr(); + return distinctCities.first; + } + + String get displayCity => getDisplayCity(isAr: false); + + String get displayUsage { + if (usage != null) { + String u = usage.toString().trim(); + if (int.tryParse(u) != null) { + return 'usage:$u'; + } + if (!u.contains('<') && !u.contains('{') && u.isNotEmpty && u.length <= 12 && u.toLowerCase() != 'online' && u.toLowerCase() != 'store') { + return 'usage:$u'; + } + } + return ''; + } + + String getDisplayValidity({bool isAr = false}) { + if (validity != null && validity!.trim().isNotEmpty && validity != "null") { + return validity!.trim(); + } + if (validTo != null && validTo!.trim().isNotEmpty && validTo != "null") { + return validTo!.trim(); + } + if (validUntil != null && validUntil!.trim().isNotEmpty && validUntil != "null") { + return validUntil!.trim(); + } + if (endDate != null && endDate!.trim().isNotEmpty && endDate != "null") { + return endDate!.trim(); + } + if (expiryDate != null && expiryDate!.trim().isNotEmpty && expiryDate != "null") { + return expiryDate!.trim(); + } + if (duration != null && duration!.trim().isNotEmpty && duration != "null") { + return duration!.trim(); + } + return LocaleKeys.offerValid.tr(); + } + + String? get resolvedEmail { + if (email != null && email!.trim().isNotEmpty && email != "null") { + return email!.trim(); + } + if (contact?.primaryEmail != null && contact!.primaryEmail!.trim().isNotEmpty && contact!.primaryEmail != "null") { + return contact!.primaryEmail!.trim(); + } + if (locations != null && locations!.isNotEmpty) { + for (var loc in locations!) { + if (loc.contact != null && loc.contact!.contains('@')) { + return loc.contact!.trim(); + } + } + } + String combined = "${description ?? ''} ${info ?? ''} ${howToUse ?? ''} ${arHowToUse ?? ''} ${enHowToUse ?? ''}"; + RegExp emailRegex = RegExp(r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}'); + Match? match = emailRegex.firstMatch(combined); + if (match != null) { + return match.group(0); + } + return null; + } + + String? get resolvedPhone { + if (phone != null && phone!.trim().isNotEmpty && phone != "null") { + return phone!.trim(); + } + if (contact?.primaryPhone != null && contact!.primaryPhone!.trim().isNotEmpty && contact!.primaryPhone != "null") { + return contact!.primaryPhone!.trim(); + } + if (locations != null && locations!.isNotEmpty) { + for (var loc in locations!) { + if (loc.contact != null && loc.contact!.trim().isNotEmpty && loc.contact != "null" && !loc.contact!.contains('@')) { + return loc.contact!.trim(); + } + } + } + String combined = "${description ?? ''} ${info ?? ''} ${howToUse ?? ''} ${arHowToUse ?? ''} ${enHowToUse ?? ''}"; + RegExp phoneRegex = RegExp(r'(?:\+?966|0)?(?:5\d{8}|1\d{7}|9200\d{5}|800\d{6,7})'); + Match? match = phoneRegex.firstMatch(combined); + if (match != null) { + return match.group(0); + } + return null; + } + + // type can be a comma-separated combo (e.g. "online,instore") when a retailer offers both - + // this splits and normalizes every part instead of only ever matching a single exact value, + // so a combined retailer is correctly recognized as both rather than neither/just one. + List get normalizedTypes { + Set result = {}; + if (type != null && type!.trim().isNotEmpty) { + for (String part in type!.split(',')) { + String t = part.trim().toLowerCase(); + if (t.isEmpty) continue; + if (t == 'online') { + result.add('online'); + } else if (t == 'instore' || t == 'in_store' || t == 'in store' || t == 'store') { + result.add('instore'); + } else { + // Non-online types (e.g. category names like "Physiotherapy") represent + // in-store merchants/services that require physical presence. + result.add('instore'); + } + } + } + if (locations != null && locations!.isNotEmpty) { + result.add('instore'); + } + if (result.isEmpty && hasPhysicalLocation) { + result.add('instore'); + } + return result.toList(); + } + + bool get isOnlineStore => normalizedTypes.contains('online'); + + bool get isInstore => normalizedTypes.contains('instore'); + + // Kept for any existing single-value callers - returns the first normalized type found. + String? get normalizedType { + List types = normalizedTypes; + return types.isEmpty ? null : types.first; + } + + String _displayTypeLabel(String norm) { + if (norm == 'online') return LocaleKeys.online.tr(); + return LocaleKeys.inStore.tr(); + } + + String? getDisplayType() { + List types = normalizedTypes; + if (types.isNotEmpty) return _displayTypeLabel(types.first); + return null; + } + + // One label per distinct type (e.g. ["Online", "In Store"]) - used to render a separate chip + // for each, instead of collapsing a dual-type retailer down to a single badge. + List getDisplayTypes() { + List types = normalizedTypes; + if (types.isNotEmpty) return types.map(_displayTypeLabel).toList(); + return []; + } +} + +class WalaPlusRetailerContact { + String? email; + List? emails; + String? phone; + List? phones; + String? website; + String? address; + String? instagram; + String? twitter; + + WalaPlusRetailerContact({this.email, this.emails, this.phone, this.phones, this.website, this.address, this.instagram, this.twitter}); + + factory WalaPlusRetailerContact.fromJson(Map json) { + List emailsList = []; + if (json['emails'] != null && json['emails'] is List) { + json['emails'].forEach((v) => emailsList.add(v.toString())); + } + String? singleEmail = json['email']?.toString() ?? json['email_address']?.toString(); + if (singleEmail != null && singleEmail.trim().isNotEmpty && singleEmail != "null") { + if (!emailsList.contains(singleEmail.trim())) { + emailsList.add(singleEmail.trim()); + } + } + + List phonesList = []; + if (json['phones'] != null && json['phones'] is List) { + json['phones'].forEach((v) => phonesList.add(v.toString())); + } + String? singlePhone = json['phone']?.toString() ?? json['mobile']?.toString() ?? json['phone_number']?.toString(); + if (singlePhone != null && singlePhone.trim().isNotEmpty && singlePhone != "null") { + if (!phonesList.contains(singlePhone.trim())) { + phonesList.add(singlePhone.trim()); + } + } + + return WalaPlusRetailerContact( + email: singleEmail, + emails: emailsList, + phone: singlePhone, + phones: phonesList, + website: WalaPlusRetailerModel.decodeUnicodeString(json['website']), + address: WalaPlusRetailerModel.decodeUnicodeString(json['address']), + instagram: json['instagram']?.toString(), + twitter: json['twitter']?.toString(), + ); + } + + Map toJson() { + return {'email': email, 'emails': emails, 'phone': phone, 'phones': phones, 'website': website, 'address': address, 'instagram': instagram, 'twitter': twitter}; + } + + String? get primaryEmail { + if (email != null && email!.trim().isNotEmpty && email != "null") return email!.trim(); + if (emails != null && emails!.isNotEmpty) { + for (var e in emails!) { + if (e.trim().isNotEmpty && e != "null") return e.trim(); + } + } + return null; + } + + String? get primaryPhone { + if (phone != null && phone!.trim().isNotEmpty && phone != "null") return phone!.trim(); + if (phones != null && phones!.isNotEmpty) { + for (var p in phones!) { + if (p.trim().isNotEmpty && p != "null") return p.trim(); + } + } + return null; + } +} + +class WalaPlusRetailerLocation { + dynamic id; + String? address; + String? city; + String? cityNameAr; + String? cityNameEn; + String? contact; + String? countryNameAr; + String? countryNameEn; + String? distance; + double? latitude; + double? longitude; + dynamic placeId; + + WalaPlusRetailerLocation({ + this.id, + this.address, + this.city, + this.cityNameAr, + this.cityNameEn, + this.contact, + this.countryNameAr, + this.countryNameEn, + this.distance, + this.latitude, + this.longitude, + this.placeId, + }); + + factory WalaPlusRetailerLocation.fromJson(Map json) { + return WalaPlusRetailerLocation( + id: json['id'], + address: WalaPlusRetailerModel.decodeUnicodeString(json['address']), + city: WalaPlusRetailerModel.decodeUnicodeString(json['city']), + cityNameAr: WalaPlusRetailerModel.decodeUnicodeString(json['city_name_ar']), + cityNameEn: WalaPlusRetailerModel.decodeUnicodeString(json['city_name_en']), + contact: json['contact']?.toString(), + countryNameAr: WalaPlusRetailerModel.decodeUnicodeString(json['country_name_ar']), + countryNameEn: WalaPlusRetailerModel.decodeUnicodeString(json['country_name_en']), + distance: WalaPlusRetailerModel.decodeUnicodeString(json['distance']), + latitude: json['latitude'] is num ? (json['latitude'] as num).toDouble() : double.tryParse(json['latitude']?.toString() ?? ''), + longitude: json['longitude'] is num ? (json['longitude'] as num).toDouble() : double.tryParse(json['longitude']?.toString() ?? ''), + placeId: json['place_id'], + ); + } + + Map toJson() { + Map data = {}; + data['id'] = id; + data['address'] = address; + data['city'] = city; + data['city_name_ar'] = cityNameAr; + data['city_name_en'] = cityNameEn; + data['contact'] = contact; + data['country_name_ar'] = countryNameAr; + data['country_name_en'] = countryNameEn; + data['distance'] = distance; + data['latitude'] = latitude; + data['longitude'] = longitude; + data['place_id'] = placeId; + return data; + } + + // distance comes from the API as a raw numeric string (e.g. "14.729670498669968") - + // round to max 2 decimal places and append the localized "KM Away" label. + String? getDisplayDistance() { + if (distance == null) return null; + double? km = double.tryParse(distance!.trim()); + if (km == null) return null; + String formatted = km.toStringAsFixed(2); + return LocaleKeys.kmAway.tr(args: [formatted]); + } + + String getLocalizedCity(bool isAr) { + if (isAr) { + if (cityNameAr != null && cityNameAr!.trim().isNotEmpty && cityNameAr != "null") { + return cityNameAr!.trim(); + } + } else { + if (cityNameEn != null && cityNameEn!.trim().isNotEmpty && cityNameEn != "null") { + return cityNameEn!.trim(); + } + } + if (city != null && city!.trim().isNotEmpty && city != "null") { + return city!.trim(); + } + return isAr ? (cityNameAr ?? "") : (cityNameEn ?? ""); + } + + String getLocalizedCountry(bool isAr) { + if (isAr) { + if (countryNameAr != null && countryNameAr!.trim().isNotEmpty && countryNameAr != "null") { + return countryNameAr!.trim(); + } + } else { + if (countryNameEn != null && countryNameEn!.trim().isNotEmpty && countryNameEn != "null") { + return countryNameEn!.trim(); + } + } + return ""; + } +} + +class WalaPlusOfferModel { + String? id; + String? retailerId; + String? title; + String? image; + String? imageThumb; + String? type; + String? description; + dynamic price; + dynamic usage; + String? expiredAt; + + WalaPlusOfferModel({this.id, this.retailerId, this.title, this.image, this.imageThumb, this.type, this.description, this.price, this.usage, this.expiredAt}); + + factory WalaPlusOfferModel.fromJson(Map json) { + return WalaPlusOfferModel( + id: json['id']?.toString(), + retailerId: (json['retailer_id'] ?? json['retailerId'] ?? json['retailerID'])?.toString(), + title: WalaPlusRetailerModel.decodeUnicodeString(json['title']), + image: json['image']?.toString(), + imageThumb: (json['image_thumb'] ?? json['imageThumb'])?.toString(), + type: json['type']?.toString(), + description: WalaPlusRetailerModel.decodeUnicodeString(json['description']), + price: json['price'], + usage: json['usage'], + expiredAt: (json['expired_at'] ?? json['expiredAt'] ?? json['expiry_date'])?.toString(), + ); + } + + Map toJson() { + return { + 'id': id, + 'retailer_id': retailerId, + 'title': title, + 'image': image, + 'image_thumb': imageThumb, + 'type': type, + 'description': description, + 'price': price, + 'usage': usage, + 'expired_at': expiredAt, + }; + } + + String? get displayImage { + if (image != null && image!.trim().isNotEmpty && image != "null") return image!.trim(); + if (imageThumb != null && imageThumb!.trim().isNotEmpty && imageThumb != "null") return imageThumb!.trim(); + return null; + } +} diff --git a/lib/models/post_params_model.dart b/lib/models/post_params_model.dart index 05356c2..1762cf6 100644 --- a/lib/models/post_params_model.dart +++ b/lib/models/post_params_model.dart @@ -1,5 +1,3 @@ -import 'package:mohem_flutter_app/app_state/app_state.dart'; - class PostParamsModel { double? versionID; int? channel; @@ -15,6 +13,7 @@ class PostParamsModel { String? payrollCodeStr; int? pSessionId; String? userName; + String? sMSSignature; PostParamsModel({ this.versionID, @@ -31,6 +30,7 @@ class PostParamsModel { this.pSelectedEmployeeNumber, this.payrollCodeStr, this.pLegislationCode, + this.sMSSignature, }); PostParamsModel.fromJson(Map json) { @@ -40,7 +40,7 @@ class PostParamsModel { mobileType = json['MobileType']; logInTokenID = json['LogInTokenID']; payrollCodeStr = json['PayrollCodeStr']; - pLegislationCode = json['LegislationCodeStr']; + pLegislationCode = json['LegislationCodeStr']?.toString().trim().toUpperCase(); tokenID = json['TokenID']; } @@ -92,7 +92,9 @@ class PostParamsModel { set setPSelectedEmployeeNumber(String? v) => pSelectedEmployeeNumber = v; - set setPLegislationCode(String? v) => pLegislationCode = v; + set setPLegislationCode(String? v) => pLegislationCode = v?.trim().toUpperCase(); set setPayrollCodeStr(String? v) => payrollCodeStr = v; + set setSMSSignature(String? v) => sMSSignature = v; + set setSmsSignature(String? v) => sMSSignature = v; } diff --git a/lib/models/privilege_list_model.dart b/lib/models/privilege_list_model.dart index 3d39118..4c49041 100644 --- a/lib/models/privilege_list_model.dart +++ b/lib/models/privilege_list_model.dart @@ -7,20 +7,23 @@ class PrivilegeListModel { int? iD; String? serviceName; bool? previlege; + bool? previlegeHMG; - PrivilegeListModel({this.iD, this.serviceName, this.previlege}); + PrivilegeListModel({this.iD, this.serviceName, this.previlege, this.previlegeHMG}); PrivilegeListModel.fromJson(Map json) { iD = json['ID']; serviceName = json['ServiceName']; - previlege = json['Previlege']; + previlege = json['Previlege'] ?? json['previlege'] ?? json['Privilege'] ?? json['privilege']; + previlegeHMG = json['Previlege_HMG'] ?? json['PrevilegeHMG'] ?? json['previlege_HMG'] ?? json['previlegeHMG'] ?? json['Privilege_HMG'] ?? json['PrivilegeHMG']; } Map toJson() { - Map data = new Map(); - data['ID'] = this.iD; - data['ServiceName'] = this.serviceName; - data['Previlege'] = this.previlege; + Map data = {}; + data['ID'] = iD; + data['ServiceName'] = serviceName; + data['Previlege'] = previlege; + data['Previlege_HMG'] = previlegeHMG; return data; } diff --git a/lib/models/profile/patient_info_model.dart b/lib/models/profile/patient_info_model.dart new file mode 100644 index 0000000..1940f08 --- /dev/null +++ b/lib/models/profile/patient_info_model.dart @@ -0,0 +1,139 @@ +class PatientInfoModel { + int? age; + String? dateOfBirth; + String? firstNameAr; + String? firstNameEn; + String? secondNameAr; + String? secondNameEn; + String? thirdNameAr; + String? thirdNameEn; + String? lastNameAr; + String? lastNameEn; + String? gender; + String? healthId; + String? idNumber; + String? idType; + bool? isHijri; + String? maritalStatus; + String? maritalStatusCode; + String? nationalDateOfBirth; + String? nationality; + String? nationalityCode; + String? occupation; + String? patientStatus; + String? placeOfBirth; + int? messageStatus; + String? errorEndUserMessage; + String? errorMessage; + + PatientInfoModel({ + this.age, + this.dateOfBirth, + this.firstNameAr, + this.firstNameEn, + this.secondNameAr, + this.secondNameEn, + this.thirdNameAr, + this.thirdNameEn, + this.lastNameAr, + this.lastNameEn, + this.gender, + this.healthId, + this.idNumber, + this.idType, + this.isHijri, + this.maritalStatus, + this.maritalStatusCode, + this.nationalDateOfBirth, + this.nationality, + this.nationalityCode, + this.occupation, + this.patientStatus, + this.placeOfBirth, + this.messageStatus, + this.errorEndUserMessage, + this.errorMessage, + }); + + factory PatientInfoModel.fromJson(Map json) { + return PatientInfoModel( + age: json['Age'], + dateOfBirth: json['DateOfBirth'], + firstNameAr: json['FirstNameAr'], + firstNameEn: json['FirstNameEn'], + secondNameAr: json['SecondNameAr'], + secondNameEn: json['SecondNameEn'], + thirdNameAr: json['ThirdNameAr'], + thirdNameEn: json['ThirdNameEn'], + lastNameAr: json['LastNameAr'], + lastNameEn: json['LastNameEn'], + gender: json['Gender'], + healthId: json['HealthId'], + idNumber: json['IdNumber'], + idType: json['IdType'], + isHijri: json['IsHijri'], + maritalStatus: json['MaritalStatus'], + maritalStatusCode: json['MaritalStatusCode'], + nationalDateOfBirth: json['NationalDateOfBirth'], + nationality: json['Nationality'], + nationalityCode: json['NationalityCode'], + occupation: json['Occupation'], + patientStatus: json['PatientStatus']?.toString(), + placeOfBirth: json['PlaceofBirth'], + messageStatus: json['MessageStatus'], + errorEndUserMessage: json['ErrorEndUserMessage'], + errorMessage: json['ErrorMessage'], + ); + } + + Map toJson() { + return { + 'Age': age, + 'DateOfBirth': dateOfBirth, + 'FirstNameAr': firstNameAr, + 'FirstNameEn': firstNameEn, + 'SecondNameAr': secondNameAr, + 'SecondNameEn': secondNameEn, + 'ThirdNameAr': thirdNameAr, + 'ThirdNameEn': thirdNameEn, + 'LastNameAr': lastNameAr, + 'LastNameEn': lastNameEn, + 'Gender': gender, + 'HealthId': healthId, + 'IdNumber': idNumber, + 'IdType': idType, + 'IsHijri': isHijri, + 'MaritalStatus': maritalStatus, + 'MaritalStatusCode': maritalStatusCode, + 'NationalDateOfBirth': nationalDateOfBirth, + 'Nationality': nationality, + 'NationalityCode': nationalityCode, + 'Occupation': occupation, + 'PatientStatus': patientStatus, + 'PlaceofBirth': placeOfBirth, + 'MessageStatus': messageStatus, + 'ErrorEndUserMessage': errorEndUserMessage, + 'ErrorMessage': errorMessage, + }; + } + + String get fullNameAr { + List parts = [ + firstNameAr ?? '', + secondNameAr ?? '', + (thirdNameAr != null && thirdNameAr != '-') ? thirdNameAr! : '', + lastNameAr ?? '', + ].where((s) => s.trim().isNotEmpty && s != '-').toList(); + return parts.join(' ').trim(); + } + + String get fullNameEn { + List parts = [ + firstNameEn ?? '', + secondNameEn ?? '', + (thirdNameEn != null && thirdNameEn != '-') ? thirdNameEn! : '', + lastNameEn ?? '', + ].where((s) => s.trim().isNotEmpty && s != '-').toList(); + return parts.join(' ').trim(); + } +} diff --git a/lib/provider/chat_call_provider.dart b/lib/provider/chat_call_provider.dart index 45205df..5eb3964 100644 --- a/lib/provider/chat_call_provider.dart +++ b/lib/provider/chat_call_provider.dart @@ -17,12 +17,12 @@ class ChatCallProvider with ChangeNotifier, DiagnosticableTreeMixin { MediaStream? _remoteStream; void initCallListeners() { - chatHubConnection.on("OnCallAcceptedAsync", onCallAcceptedAsync); - chatHubConnection.on("OnIceCandidateAsync", onIceCandidateAsync); - chatHubConnection.on("OnOfferAsync", onOfferAsync); - chatHubConnection.on("OnAnswerOffer", onAnswerOffer); - chatHubConnection.on("OnHangUpAsync", onHangUpAsync); - chatHubConnection.on("OnCallDeclinedAsync", onCallDeclinedAsync); + chatHubConnection?.on("OnCallAcceptedAsync", onCallAcceptedAsync); + chatHubConnection?.on("OnIceCandidateAsync", onIceCandidateAsync); + chatHubConnection?.on("OnOfferAsync", onOfferAsync); + chatHubConnection?.on("OnAnswerOffer", onAnswerOffer); + chatHubConnection?.on("OnHangUpAsync", onHangUpAsync); + chatHubConnection?.on("OnCallDeclinedAsync", onCallDeclinedAsync); } //Video Constraints @@ -175,13 +175,13 @@ class ChatCallProvider with ChangeNotifier, DiagnosticableTreeMixin { args = [targetUserID, data]; //json In Data } - await chatHubConnection.invoke(invokeMethod, args: args); + await chatHubConnection?.invoke(invokeMethod, args: args); } void stopListeners() async { - chatHubConnection.off('OnCallDeclinedAsync'); - chatHubConnection.off('OnCallAcceptedAsync'); - chatHubConnection.off('OnIceCandidateAsync'); - chatHubConnection.off('OnAnswerOffer'); + chatHubConnection?.off('OnCallDeclinedAsync'); + chatHubConnection?.off('OnCallAcceptedAsync'); + chatHubConnection?.off('OnIceCandidateAsync'); + chatHubConnection?.off('OnAnswerOffer'); } } diff --git a/lib/provider/chat_provider_model.dart b/lib/provider/chat_provider_model.dart index 232df22..b505539 100644 --- a/lib/provider/chat_provider_model.dart +++ b/lib/provider/chat_provider_model.dart @@ -16,6 +16,8 @@ import 'package:mohem_flutter_app/app_state/app_state.dart'; import 'package:mohem_flutter_app/classes/consts.dart'; import 'package:mohem_flutter_app/classes/encryption.dart'; import 'package:mohem_flutter_app/classes/utils.dart'; +import 'package:mohem_flutter_app/generated/locale_keys.g.dart'; +import 'package:mohem_flutter_app/widgets/bottom_sheets/confirm_bottom_sheet.dart'; import 'package:mohem_flutter_app/config/routes.dart'; import 'package:mohem_flutter_app/main.dart'; import 'package:mohem_flutter_app/models/chat/chat_user_image_model.dart'; @@ -113,16 +115,16 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { Future buildHubConnection() async { chatHubConnection = await getHubConnection(); - await chatHubConnection.start(); + await chatHubConnection?.start(); if (kDebugMode) { logger.i("Hub Conn: Startedddddddd"); } - chatHubConnection.on("OnDeliveredChatUserAsync", onMsgReceived); - chatHubConnection.on("OnGetChatConversationCount", onNewChatConversion); + chatHubConnection?.on("OnDeliveredChatUserAsync", onMsgReceived); + chatHubConnection?.on("OnGetChatConversationCount", onNewChatConversion); //group On message - chatHubConnection.on("OnDeliveredGroupChatHistoryAsync", onGroupMsgReceived); + chatHubConnection?.on("OnDeliveredGroupChatHistoryAsync", onGroupMsgReceived); } Future getHubConnection() async { @@ -135,16 +137,16 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { } void registerEvents() { - chatHubConnection.on("OnUpdateUserStatusAsync", changeStatus); - // chatHubConnection.on("OnDeliveredChatUserAsync", onMsgReceived); + chatHubConnection?.on("OnUpdateUserStatusAsync", changeStatus); + // chatHubConnection?.on("OnDeliveredChatUserAsync", onMsgReceived); - chatHubConnection.on("OnSubmitChatAsync", OnSubmitChatAsync); - chatHubConnection.on("OnUserTypingAsync", onUserTyping); - chatHubConnection.on("OnUserCountAsync", userCountAsync); - // chatHubConnection.on("OnUpdateUserChatHistoryWindowsAsync", updateChatHistoryWindow); - chatHubConnection.on("OnGetUserChatHistoryNotDeliveredAsync", chatNotDelivered); - chatHubConnection.on("OnUpdateUserChatHistoryStatusAsync", updateUserChatStatus); - chatHubConnection.on("OnGetGroupUserStatusAsync", getGroupUserStatus); + chatHubConnection?.on("OnSubmitChatAsync", OnSubmitChatAsync); + chatHubConnection?.on("OnUserTypingAsync", onUserTyping); + chatHubConnection?.on("OnUserCountAsync", userCountAsync); + // chatHubConnection?.on("OnUpdateUserChatHistoryWindowsAsync", updateChatHistoryWindow); + chatHubConnection?.on("OnGetUserChatHistoryNotDeliveredAsync", chatNotDelivered); + chatHubConnection?.on("OnUpdateUserChatHistoryStatusAsync", updateUserChatStatus); + chatHubConnection?.on("OnGetGroupUserStatusAsync", getGroupUserStatus); // // {"type":1,"target":"","arguments":[[{"id":217869,"userName":"Sultan.Khan","email":"Sultan.Khan@cloudsolutions.com.sa","phone":null,"title":"Sultan.Khan","userStatus":1,"image":null,"unreadMessageCount":0,"userAction":3,"isPin":false,"isFav":false,"isAdmin":false,"rKey":null,"totalCount":0,"isHuaweiDevice":false,"deviceToken":null},{"id":15153,"userName":"Tamer.Fanasheh","email":"Tamer.F@cloudsolutions.com.sa","phone":null,"title":"Tamer Fanasheh","userStatus":2,"image":null,"unreadMessageCount":0,"userAction":3,"isPin":false,"isFav":false,"isAdmin":true,"rKey":null,"totalCount":0,"isHuaweiDevice":false,"deviceToken":null}]]} @@ -183,7 +185,7 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { } Future invokeUserChatHistoryNotDeliveredAsync({required int userId}) async { - await chatHubConnection.invoke("GetUserChatHistoryNotDeliveredAsync", args: [userId]); + await chatHubConnection?.invoke("GetUserChatHistoryNotDeliveredAsync", args: [userId]); return ""; } @@ -252,7 +254,7 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { void updateUserChatHistoryStatusAsync(List data) { try { - chatHubConnection.invoke("UpdateUserChatHistoryStatusAsync", args: [data]); + chatHubConnection?.invoke("UpdateUserChatHistoryStatusAsync", args: [data]); } catch (e) { throw e; } @@ -260,7 +262,7 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { void updateUserChatHistoryOnMsg(List data) { try { - chatHubConnection.invoke("UpdateUserChatHistoryStatusAsync", args: [data]); + chatHubConnection?.invoke("UpdateUserChatHistoryStatusAsync", args: [data]); } catch (e) { throw e; } @@ -753,7 +755,7 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { String chatData = '{"contant":"$msg","contantNo":"$contentNo","chatEventId":$chatEventId,"fileTypeId": $fileTypeId,"currentUserId":${AppState().chatDetails!.response!.id},"chatSource":1,"userChatHistoryLineRequestList":[{"isSeen":false,"isDelivered":false,"targetUserId":$targetUserId,"targetUserStatus":1}],"chatReplyId":$chatReplyId,"conversationId":"$chatCID"}'; - await chatHubConnection.invoke("AddChatUserAsync", args: [json.decode(chatData)]); + await chatHubConnection?.invoke("AddChatUserAsync", args: [json.decode(chatData)]); } //groupChatMessage @@ -829,7 +831,7 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { String chatData = '{"contant":"$msg","contantNo":"$contentNo","chatEventId":$chatEventId,"fileTypeId":$fileTypeId,"currentUserId":${AppState().chatDetails!.response!.id},"chatSource":1,"groupId":$targetGroupId,"groupChatHistoryLineRequestList":${json.encode(targetUsers)},"chatReplyId": $chatReplyId,"conversationId":"${uuid.v4()}"}'; - await chatHubConnection.invoke("AddGroupChatHistoryAsync", args: [json.decode(chatData)]); + await chatHubConnection?.invoke("AddGroupChatHistoryAsync", args: [json.decode(chatData)]); } void sendGroupChatMessage( @@ -1450,7 +1452,14 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { pChatHistory?.clear(); uGroups?.clear(); searchGroup?.clear(); - chatHubConnection.stop(); + try { + chatHubConnection?.stop(); + } catch (e) { + if (kDebugMode) { + print("Error stopping chatHubConnection: $e"); + } + } + chatHubConnection = null; AppState().chatDetails = null; } } @@ -1582,17 +1591,17 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { } Future invokeChatCounter({required int userId}) async { - await chatHubConnection.invoke("GetChatCounversationCount", args: [userId]); + await chatHubConnection?.invoke("GetChatCounversationCount", args: [userId]); return ""; } void userTypingInvoke({required int currentUser, required int reciptUser}) async { - await chatHubConnection.invoke("UserTypingAsync", args: [reciptUser, currentUser]); + await chatHubConnection?.invoke("UserTypingAsync", args: [reciptUser, currentUser]); } void groupTypingInvoke({required GroupResponse groupDetails, required int groupId}) async { var data = json.decode(json.encode(groupDetails.groupUserList)); - await chatHubConnection.invoke("GroupTypingAsync", args: ["${groupDetails.adminUser!.userName}", data, groupId]); + await chatHubConnection?.invoke("GroupTypingAsync", args: ["${groupDetails.adminUser!.userName}", data, groupId]); } //////// Audio Recoding Work //////////////////// @@ -1631,20 +1640,22 @@ class ChatProviderModel with ChangeNotifier, DiagnosticableTreeMixin { void startRecoding(BuildContext context) async { await Permission.microphone.request().then((PermissionStatus status) { if (status.isPermanentlyDenied) { - Utils.confirmDialog( + ConfirmBottomSheet.show( context, - "The app needs microphone access to be able to record audio.", - onTap: () { - Navigator.of(context).pop(); + message: LocaleKeys.microphoneAccessRequired.tr(), + confirmText: LocaleKeys.openSettings.tr(), + showCancelButton: false, + onConfirm: () { openAppSettings(); }, ); } else if (status.isDenied) { - Utils.confirmDialog( + ConfirmBottomSheet.show( context, - "The app needs microphone access to be able to record audio.", - onTap: () { - Navigator.of(context).pop(); + message: LocaleKeys.microphoneAccessRequired.tr(), + confirmText: LocaleKeys.openSettings.tr(), + showCancelButton: false, + onConfirm: () { openAppSettings(); }, ); diff --git a/lib/provider/dashboard_provider_model.dart b/lib/provider/dashboard_provider_model.dart index 904ef5d..abc3d70 100644 --- a/lib/provider/dashboard_provider_model.dart +++ b/lib/provider/dashboard_provider_model.dart @@ -27,7 +27,6 @@ import 'package:mohem_flutter_app/models/greetings/greeting_card_model.dart'; import 'package:mohem_flutter_app/models/itg/itg_response_model.dart'; import 'package:mohem_flutter_app/models/offers_and_discounts/get_offers_list.dart'; import 'package:mohem_flutter_app/models/sso_auth_model.dart'; -import 'package:mohem_flutter_app/widgets/dialogs/confirm_dialog.dart'; /// Mix-in [DiagnosticableTreeMixin] to have access to [debugFillProperties] for the devtool // ignore: prefer_mixin @@ -190,15 +189,11 @@ class DashboardProviderModel with ChangeNotifier, DiagnosticableTreeMixin { notifyListeners(); Utils.handleException(ex, context, (err) { Utils.hideLoading(context); - showDialog( - context: context, - builder: - (cxt) => ConfirmDialog( - message: err, - onTap: () { - Navigator.pushNamedAndRemoveUntil(cxt, AppRoutes.login, (Route route) => false); - }, - ), + Utils.showToast( + err, + onComplete: () { + Navigator.pushNamedAndRemoveUntil(context, AppRoutes.login, (Route route) => false); + }, ); }); } diff --git a/lib/ui/attendance/add_vacation_rule_screen.dart b/lib/ui/attendance/add_vacation_rule_screen.dart index 5d4bf75..4d8d595 100644 --- a/lib/ui/attendance/add_vacation_rule_screen.dart +++ b/lib/ui/attendance/add_vacation_rule_screen.dart @@ -24,7 +24,7 @@ import 'package:mohem_flutter_app/widgets/app_bar_widget.dart'; import 'package:mohem_flutter_app/widgets/bottom_sheet.dart'; import 'package:mohem_flutter_app/widgets/bottom_sheets/search_employee_bottom_sheet.dart'; import 'package:mohem_flutter_app/widgets/button/default_button.dart'; -import 'package:mohem_flutter_app/widgets/dialogs/confirm_dialog.dart'; +import 'package:mohem_flutter_app/widgets/bottom_sheets/confirm_bottom_sheet.dart'; import 'package:mohem_flutter_app/widgets/dynamic_forms/dynamic_textfield_widget.dart'; import 'package:mohem_flutter_app/widgets/item_detail_view_widget.dart'; @@ -216,7 +216,7 @@ class _AddVacationRuleScreenState extends State { CreateVacationRuleList? createVacationRuleList = await VacationRuleApiClient().createVacationRule(DateUtil.convertDateToStringLocation(startTime), DateUtil.convertDateToStringLocation(endTime!), selectedItemType!.iTEMTYPE!, selectedItemTypeNotification!.nOTIFICATIONNAME!, message, getPAction(), selectedReplacementEmployee!.userName!, respondAttributeList); Utils.hideLoading(context); - Utils.showToast("Vacation rule added"); + Utils.showToast(LocaleKeys.vacationRuleAdded.tr(), isSuccess: true); Navigator.popUntil(context, ModalRoute.withName(AppRoutes.dashboard)); } catch (ex) { Utils.hideLoading(context); @@ -490,15 +490,14 @@ class _AddVacationRuleScreenState extends State { } } - showDialog( - context: context, - builder: (cxt) => ConfirmDialog( - message: LocaleKeys.areYouSureYouWantToSubmit.tr(), - onTap: () { - Navigator.pop(context); - createVacationRule(list); - }, - ), + ConfirmBottomSheet.show( + context, + message: LocaleKeys.areYouSureYouWantToSubmit.tr(), + confirmText: LocaleKeys.yes.tr(), + cancelText: LocaleKeys.no.tr(), + onConfirm: () { + createVacationRule(list); + }, ); }, ).insideContainer, diff --git a/lib/ui/chat/chat_home.dart b/lib/ui/chat/chat_home.dart index c522475..3a64357 100644 --- a/lib/ui/chat/chat_home.dart +++ b/lib/ui/chat/chat_home.dart @@ -27,14 +27,14 @@ class ChatHome extends StatefulWidget { class _ChatHomeState extends State { int tabIndex = 0; PageController controller = PageController(); - late ChatProviderModel data; + late ChatProviderModel data; @override void initState() { super.initState(); - if (chatHubConnection.state == HubConnectionState.Connected) { - data = Provider.of(context, listen: false); + data = Provider.of(context, listen: false); + if (chatHubConnection?.state == HubConnectionState.Connected) { data.registerEvents(); } } @@ -46,7 +46,7 @@ class _ChatHomeState extends State { } void fetchAgain() { - if (chatHubConnection.state != HubConnectionState.Connected) { + if (chatHubConnection?.state != HubConnectionState.Connected) { data.getUserAutoLoginToken().whenComplete(() async { await data.buildHubConnection(); data.getUserRecentChats(); diff --git a/lib/ui/chat/group_chat.dart b/lib/ui/chat/group_chat.dart index 35b4c5c..575dfa0 100644 --- a/lib/ui/chat/group_chat.dart +++ b/lib/ui/chat/group_chat.dart @@ -19,6 +19,7 @@ import 'package:mohem_flutter_app/ui/chat/create_group.dart'; import 'package:mohem_flutter_app/ui/chat/group_chat_detaied_screen.dart'; import 'package:mohem_flutter_app/ui/chat/manage_group.dart'; import 'package:mohem_flutter_app/widgets/bottom_sheet.dart'; +import 'package:mohem_flutter_app/widgets/bottom_sheets/confirm_bottom_sheet.dart'; import 'package:mohem_flutter_app/widgets/bottom_sheets/search_employee_bottom_sheet.dart'; import 'package:mohem_flutter_app/widgets/shimmer/dashboard_shimmer_widget.dart'; import 'package:provider/provider.dart'; @@ -269,11 +270,13 @@ void goToSelected(GroupResponse? groupDetails, ChatProviderModel m, String value void deleteGroup( GroupResponse? groupDetails, ChatProviderModel m, BuildContext context) { groupDetails!.groupUserList; - Utils.confirmDialog( + ConfirmBottomSheet.show( context, - LocaleKeys.areYouSureWantTodelete.tr(), - onTap: () { - Navigator.pop(context); + message: LocaleKeys.areYouSureWantTodelete.tr(), + confirmText: LocaleKeys.yes.tr(), + cancelText: LocaleKeys.no.tr(), + isDestructive: true, + onConfirm: () { m.deleteGroup(groupDetails); }, ); diff --git a/lib/ui/dialogs/id/business_card_dialog.dart b/lib/ui/dialogs/id/business_card_dialog.dart index a405ccf..d62b826 100644 --- a/lib/ui/dialogs/id/business_card_dialog.dart +++ b/lib/ui/dialogs/id/business_card_dialog.dart @@ -1,15 +1,19 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.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/utils.dart'; import 'package:mohem_flutter_app/extensions/int_extensions.dart'; import 'package:mohem_flutter_app/extensions/string_extensions.dart'; import 'package:mohem_flutter_app/extensions/widget_extensions.dart'; import 'package:mohem_flutter_app/generated/locale_keys.g.dart'; +import 'package:mohem_flutter_app/models/generic_response_model.dart'; class BusinessCardDialog extends StatelessWidget { - const BusinessCardDialog({Key? key}) : super(key: key); + final String? qrBase64; + final EmpBusinessCardInfoModel? employeeInfo; + + const BusinessCardDialog({Key? key, this.qrBase64, this.employeeInfo}) + : super(key: key); @override Widget build(BuildContext context) { @@ -17,65 +21,78 @@ class BusinessCardDialog extends StatelessWidget { quarterTurns: 1, child: AspectRatio( aspectRatio: 3.5 / 2, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Image.asset( + "assets/images/logos/bn_cloud_soloution.jpg", + width: 94, + height: 91.15, + ).paddingOnly(top: 4), + ], + ), + ), + Image.memory( + Utils.getPostBytes(qrBase64 ?? ""), + width: 129, + height: 129, + ), + ], + ).paddingOnly(top: 26, left: 8.5), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + (employeeInfo?.fullName ?? "") + .toText23(isBold: true) + .paddingOnly(top: 15), + (employeeInfo?.pPositionTitle ?? "").toText17( + color: MyColors.grey57Color, + ), + 30.height, + Row( + children: [ + const Icon( + Icons.email, + size: 17.5, + color: MyColors.grey3AColor, + ).paddingOnly(right: 11.5), + ("${LocaleKeys.email.tr()}: ").toText18( + color: MyColors.grey57Color, + ), + (employeeInfo?.pEmailAddress ?? "").toText18(), + ], + ), + Row( children: [ - Image.asset( - "assets/images/logos/bn_cloud_soloution.jpg", - width: 94, - height: 91.15, - ).paddingOnly(top: 4), + const Icon( + Icons.call, + size: 17.5, + color: MyColors.grey3AColor, + ).paddingOnly(right: 11.5), + ("${LocaleKeys.mobile.tr()}: ").toText18( + color: MyColors.grey57Color, + ), + (employeeInfo?.pMobilePhoneNumber ?? "").toText18(), ], ), - ), - Image.memory( - Utils.getPostBytes(AppState().memberInformationList!.businessCardQR ?? ""), - width: 129, - height: 129, - ), - ], - ).paddingOnly(top: 26, left: 8.5), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - (AppState().memberInformationList!.eMPLOYEENAME ?? "").toText23(isBold: true).paddingOnly(top: 15), - (AppState().memberInformationList!.getPositionName() ?? "").toText17( - color: MyColors.grey57Color, - ), - 30.height, - // (AppState().memberInformationList!.eMPLOYEENUMBER ?? "").toText13(), - Row( - children: [ - const Icon(Icons.email, size: 17.5, color: MyColors.grey3AColor).paddingOnly(right: 11.5), - ("${LocaleKeys.email.tr()}: ").toText18(color: MyColors.grey57Color), - (AppState().memberInformationList!.eMPLOYEEEMAILADDRESS ?? "").toText18(), - ], - ), - Row( - children: [ - const Icon(Icons.call, size: 17.5, color: MyColors.grey3AColor).paddingOnly(right: 11.5), - ("${LocaleKeys.mobile.tr()}: ").toText18(color: MyColors.grey57Color), - (AppState().memberInformationList!.mobileNumberWithZipCode ?? "").toText18(), - ], - ), - // 1.height, - // (AppState().memberInformationList!.jOBNAME ?? "").toText13(), - ], - ), - ], - ).paddingOnly(right: 13, left: 32, bottom: 28), + ], + ), + ], + ).paddingOnly(right: 13, left: 32, bottom: 28), + ), ), ); } diff --git a/lib/ui/etqan_ovr/etqan_create_request.dart b/lib/ui/etqan_ovr/etqan_create_request.dart index ba3c504..8ab7380 100644 --- a/lib/ui/etqan_ovr/etqan_create_request.dart +++ b/lib/ui/etqan_ovr/etqan_create_request.dart @@ -87,23 +87,11 @@ class _EtqanOvrCreateRequestState extends State { DefaultButton(LocaleKeys.submit.tr(), () async { provider.setDescription(_descriptionController.text); if (provider.selectedProject == null) { - Utils.showErrorDialog( - context: context, - message: LocaleKeys.pleaseSelectProject.tr(), - onOkTapped: () { - Navigator.of(context).pop(); - }, - ); + Utils.showToast(LocaleKeys.pleaseSelectProject.tr()); return; } if (!provider.isDescriptionValid()) { - Utils.showErrorDialog( - context: context, - message: LocaleKeys.pleaseEnterDescription.tr(), - onOkTapped: () { - Navigator.of(context).pop(); - }, - ); + Utils.showToast(LocaleKeys.pleaseEnterDescription.tr()); return; } Map? ticketInfo = await provider.submitRequest(context); @@ -112,15 +100,10 @@ class _EtqanOvrCreateRequestState extends State { String ticketId = ticketInfo['id'] ?? ''; // String successMessage = '${LocaleKeys.requestCreatedSuccessfully.tr()}\n\n${LocaleKeys.ticketNumber.tr()}: $ticketNumber\n${LocaleKeys.ticketId.tr()}: $ticketId'; String successMessage = '${LocaleKeys.requestCreatedSuccessfully.tr()}\n\n${LocaleKeys.ticketNumber.tr()}: $ticketNumber'; - Utils.showErrorDialog( - context: context, - message: successMessage, - onOkTapped: () { - Navigator.pop(context); - Navigator.pop(context); - // Navigator.popAndPushNamed(context, AppRoutes.etqanOvr); - }, - onCloseTap: () { + Utils.showToast( + successMessage, + isSuccess: true, + onComplete: () { Navigator.pop(context); Navigator.pop(context); // Navigator.popAndPushNamed(context, AppRoutes.etqanOvr); diff --git a/lib/ui/landing/dashboard_screen.dart b/lib/ui/landing/dashboard_screen.dart index 6940149..6294d76 100644 --- a/lib/ui/landing/dashboard_screen.dart +++ b/lib/ui/landing/dashboard_screen.dart @@ -30,12 +30,13 @@ import 'package:mohem_flutter_app/widgets/bottom_sheet.dart'; import 'package:mohem_flutter_app/widgets/mark_attendance_widget.dart'; import 'package:mohem_flutter_app/widgets/shimmer/dashboard_shimmer_widget.dart'; import 'package:mohem_flutter_app/widgets/shimmer/offers_shimmer_widget.dart'; + import 'package:provider/provider.dart'; import 'package:pull_to_refresh/pull_to_refresh.dart'; import 'package:signalr_netcore/signalr_client.dart'; import 'package:url_launcher/url_launcher.dart'; -late HubConnection chatHubConnection; +HubConnection? chatHubConnection; class DashboardScreen extends StatefulWidget { DashboardScreen({Key? key}) : super(key: key); @@ -56,12 +57,13 @@ class _DashboardScreenState extends State with WidgetsBindingOb int currentIndex = 0; - bool isDisplayMazaya = false; + bool isDisplayMazaya = true; @override void initState() { WidgetsBinding.instance.addObserver(this); super.initState(); + isDisplayMazaya = checkIfPrivilegedForMazaya(); cProvider = Provider.of(context, listen: false); scheduleMicrotask(() { data = Provider.of(context, listen: false); @@ -93,7 +95,7 @@ class _DashboardScreenState extends State with WidgetsBindingOb WidgetsBinding.instance.removeObserver(this); super.dispose(); if (!cProvider.disbaleChatForThisUser) { - chatHubConnection.stop(); + chatHubConnection?.stop(); } } @@ -120,16 +122,16 @@ class _DashboardScreenState extends State with WidgetsBindingOb Future checkHubCon() async { // chatHubConnection = await context.read().getHubConnection(); - if (chatHubConnection.state == HubConnectionState.Connected) { - await chatHubConnection.stop(); - await chatHubConnection.start(); - } else if (chatHubConnection.state != HubConnectionState.Connected) { - await chatHubConnection.start(); + if (chatHubConnection?.state == HubConnectionState.Connected) { + await chatHubConnection?.stop(); + await chatHubConnection?.start(); + } else if (chatHubConnection != null && chatHubConnection!.state != HubConnectionState.Connected) { + await chatHubConnection?.start(); } } void gotoChat(BuildContext context) async { - if (chatHubConnection.state == HubConnectionState.Connected) { + if (chatHubConnection?.state == HubConnectionState.Connected) { Utils.hideLoading(context); Navigator.pushNamed(context, AppRoutes.chat); String isAppOpendByChat = await Utils.getStringFromPrefs("isAppOpendByChat"); @@ -254,6 +256,7 @@ class _DashboardScreenState extends State with WidgetsBindingOb @override Widget build(BuildContext context) { + isDisplayMazaya = checkIfPrivilegedForMazaya(); return SafeArea( bottom: Platform.isAndroid ? true : false, top: false, @@ -290,9 +293,7 @@ class _DashboardScreenState extends State with WidgetsBindingOb }); }, ), - Image - .asset("assets/images/logos/main_mohemm_logo.png", width: 134, height: 28) - .expanded, + Image.asset("assets/images/logos/main_mohemm_logo.png", width: 134, height: 28).expanded, SvgPicture.asset("assets/images/announcements.svg", matchTextDirection: true).onPress(() async { await Navigator.pushNamed(context, AppRoutes.announcements); }), @@ -453,232 +454,120 @@ class _DashboardScreenState extends State with WidgetsBindingOb return const SizedBox(); } }, - ).paddingOnly(bottom: 24, left: 24, right: 24), + ).paddingOnly(bottom: 18, left: 24, right: 24), // eventActivityWidget(context), - if (isDisplayMazaya) ...[ + if (isDisplayMazaya) ...[ + InkWell( + borderRadius: BorderRadius.circular(20), + onTap: () { + Navigator.pushNamed(context, AppRoutes.mazayaDiscover); + }, + child: ClipRRect( + borderRadius: BorderRadius.circular(20), + child: AspectRatio( + aspectRatio: 386 / 121.37, + child: SvgPicture.asset( + AppState().isArabic(context) ? "assets/images/mazaya_banner_ar.svg" : "assets/images/mazaya_banner_en.svg", + width: double.infinity, + fit: BoxFit.fill, + ), + ), + ), + ).paddingOnly(left: 21, right: 21, top: 0, bottom: 21), + ] else ...[ Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Directionality( - textDirection: AppState().isArabic(context) ? ui.TextDirection.rtl : ui.TextDirection.ltr, - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - gradient: const LinearGradient(colors: [Color(0xFF91C481), Color(0xFF7CCED7)], begin: Alignment.centerLeft, end: Alignment.centerRight), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + LocaleKeys.offers.tr().toText12(), + Row( + children: [ + LocaleKeys.discounts.tr().toText24(isBold: true), + 6.width, + Container( + padding: const EdgeInsets.only(left: 8, right: 8), + decoration: BoxDecoration(color: MyColors.yellowColor, borderRadius: BorderRadius.circular(10)), + child: LocaleKeys.newString.tr().toText10(isBold: true), + ), + ], + ), + ], + ), ), - child: Padding( - padding: const EdgeInsets.all(3.0), // This creates the border width - child: Container( - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(17), // Slightly less than outer radius - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - flex: 4, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Row( + LocaleKeys.viewAllOffers.tr().toText12(isUnderLine: true).onPress(() { + Navigator.pushNamed(context, AppRoutes.offersAndDiscounts); + }), + ], + ).paddingOnly(left: 21, right: 21), + Consumer( + builder: (BuildContext context, DashboardProviderModel model, Widget? child) { + return SizedBox( + height: 103 + 33, + child: ListView.separated( + shrinkWrap: true, + physics: const BouncingScrollPhysics(), + padding: const EdgeInsets.only(left: 21, right: 21, top: 13), + scrollDirection: Axis.horizontal, + itemBuilder: (BuildContext cxt, int index) { + return model.isOffersLoading + ? const OffersShimmerWidget() + : data.getOffersList.isNotEmpty + ? InkWell( + onTap: () { + navigateToDetails(data.getOffersList[index]); + }, + child: SizedBox( + width: 73, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, children: [ - Expanded( - flex: 2, - child: RichText( - text: - AppState().isArabic(context) - ? TextSpan( - children: [ - TextSpan( - text: 'اطلع على مميزات', - style: TextStyle( - fontSize: 16, - letterSpacing: -0.2, - fontFamily: AppState().isArabic(context) ? 'Cairo' : 'Poppins', - fontWeight: FontWeight.w700, - height: 24 / 16, - color: const Color(0xFF5D5E5E), - ), - ), - TextSpan( - text: ' مزايا', - style: TextStyle( - fontSize: 16, - fontFamily: AppState().isArabic(context) ? 'Cairo' : 'Poppins', - fontWeight: FontWeight.w700, - letterSpacing: -0.2, - height: 24 / 16, - color: MyColors.mazayaRedColor, // Use your MAZAYA red color here if defined, e.g. MyColors.mazayaRed - ), - ), - ], - ) - : TextSpan( - children: [ - TextSpan( - text: LocaleKeys.explore.tr() + ' ', - style: const TextStyle( - fontSize: 16, - letterSpacing: -0.2, - fontFamily: 'Poppins', - fontWeight: FontWeight.w700, - height: 24 / 16, - color: Color(0xFF5D5E5E), - ), - ), - TextSpan( - text: LocaleKeys.mazaya.tr(), - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w700, - fontFamily: 'Poppins', - letterSpacing: -0.2, - height: 24 / 16, - color: MyColors.mazayaRedColor, // Use your MAZAYA red color here if defined, e.g. MyColors.mazayaRed - ), - ), - TextSpan( - text: ' ' + LocaleKeys.benefits.tr(), - style: const TextStyle( - fontSize: 16, - letterSpacing: -0.2, - fontFamily: 'Poppins', - fontWeight: FontWeight.w700, - height: 24 / 16, - color: Color(0xFF5D5E5E), - ), - ), - ], - ), + Container( + width: 73, + height: 73, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(100)), + border: Border.all(color: MyColors.lightGreyE3Color, width: 1), + ), + child: ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(50)), + child: Hero( + tag: "ItemImage" + data.getOffersList[index].offersDiscountId.toString(), + transitionOnUserGestures: true, + child: Image.network(data.getOffersList[index].logo ?? "", fit: BoxFit.contain), + ), ), ), - const Expanded(flex: 1, child: SizedBox()), + 4.height, + Expanded( + child: + AppState().isArabic(context) + ? data.getOffersList[index].titleAr!.toText12(isCenter: true, maxLine: 1) + : data.getOffersList[index].titleEn!.toText12(isCenter: true, maxLine: 1), + ), ], ), - const SizedBox(height: 8), - LocaleKeys.mazayaDesc.tr().toText11(color: const Color(0xFF5D5E5E)), - ], - ), - ), - Expanded( - flex: 2, - child: Column( - crossAxisAlignment: CrossAxisAlignment.end, - mainAxisAlignment: MainAxisAlignment.end, - children: [ - SvgPicture.asset("assets/icons/mazaya_brand.svg", width: 90, height: 47), - const SizedBox(height: 28), - LocaleKeys.viewallofferMazaya.tr().toText12(isUnderLine: true, color: const Color(0xFF3B3D4A)).onPress(() { - Navigator.pushNamed(context, AppRoutes.offersAndDiscounts); - }), - ], - ), - ), - ], - ).paddingOnly(left: 21, right: 21, top: 14, bottom: 14), + ), + ) + : Container(); + }, + separatorBuilder: (BuildContext cxt, int index) => 8.width, + itemCount: model.isOffersLoading ? 9 : (data.getOffersList.length > 9 ? 9 : data.getOffersList.length), ), - ), - ).paddingOnly(left: 21, right: 21, top: 0, bottom: 21), + ); + }, ), ], ), ], - - Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - LocaleKeys.offers.tr().toText12(), - Row( - children: [ - LocaleKeys.discounts.tr().toText24(isBold: true), - 6.width, - Container( - padding: const EdgeInsets.only(left: 8, right: 8), - decoration: BoxDecoration(color: MyColors.yellowColor, borderRadius: BorderRadius.circular(10)), - child: LocaleKeys.newString.tr().toText10(isBold: true), - ), - ], - ), - ], - ), - ), - LocaleKeys.viewAllOffers.tr().toText12(isUnderLine: true).onPress(() { - Navigator.pushNamed(context, AppRoutes.offersAndDiscounts); - }), - ], - ).paddingOnly(left: 21, right: 21), - Consumer( - builder: (BuildContext context, DashboardProviderModel model, Widget? child) { - return SizedBox( - height: 103 + 33, - child: ListView.separated( - shrinkWrap: true, - physics: const BouncingScrollPhysics(), - padding: const EdgeInsets.only(left: 21, right: 21, top: 13), - scrollDirection: Axis.horizontal, - itemBuilder: (BuildContext cxt, int index) { - return model.isOffersLoading - ? const OffersShimmerWidget() - : InkWell( - onTap: () { - navigateToDetails(data.getOffersList[index]); - }, - child: SizedBox( - width: 73, - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Container( - width: 73, - height: 73, - decoration: BoxDecoration( - color: Colors.white, - borderRadius: const BorderRadius.all(Radius.circular(100)), - border: Border.all(color: MyColors.lightGreyE3Color, width: 1), - ), - child: ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(50)), - child: Hero( - tag: "ItemImage" + data.getOffersList[index].offersDiscountId.toString()!, - transitionOnUserGestures: true, - child: Image.network(data.getOffersList[index].logo ?? "", fit: BoxFit.contain), - ), - ), - ), - 4.height, - Expanded( - child: - AppState().isArabic(context) - ? data.getOffersList[index].titleAr!.toText12(isCenter: true, maxLine: 1) - : data.getOffersList[index].titleEn!.toText12(isCenter: true, maxLine: 1), - ), - ], - ), - ), - ); - }, - separatorBuilder: (BuildContext cxt, int index) => 8.width, - itemCount: 9, - ), - ); - }, - ), - ], - ), - Container( width: double.infinity, padding: const EdgeInsets.only(top: 31), @@ -691,9 +580,7 @@ class _DashboardScreenState extends State with WidgetsBindingOb mainAxisSize: MainAxisSize.min, children: [ ServicesWidget(), - context - .watch() - .isLoading ? const MarathonBannerShimmer().paddingAll(20) : const MarathonBanner().paddingOnly(left: 21, right: 21, bottom: 8, top: 8), + context.watch().isLoading ? const MarathonBannerShimmer().paddingAll(20) : const MarathonBanner().paddingOnly(left: 21, right: 21, bottom: 8, top: 8), // context.watch().isTutorialLoading // ? const MarathonBannerShimmer().paddingAll(20) // : Container( @@ -867,16 +754,9 @@ class _DashboardScreenState extends State with WidgetsBindingOb } Widget eventActivityWidget(BuildContext context) { - return (context - .watch() - .isEventLoadingLoading) + return (context.watch().isEventLoadingLoading) ? const MarathonBannerShimmer().paddingOnly(left: 21, right: 21, bottom: 21, top: 0) - : (context - .watch() - .eventActivity != null && context - .watch() - .eventActivity! - .isActive == true) + : (context.watch().eventActivity != null && context.watch().eventActivity!.isActive == true) ? const EventActivityBanner().paddingOnly(left: 21, right: 21, bottom: 21, top: 0) : const SizedBox(); } @@ -909,4 +789,14 @@ class _DashboardScreenState extends State with WidgetsBindingOb } return false; } + + bool checkIfPrivilegedForMazaya() { + if (AppState().privilegeListModel == null) return false; + for (PrivilegeListModel element in AppState().privilegeListModel!) { + if (element.serviceName?.toLowerCase() == "mazayaeligibility") { + return (element.previlege ?? false) || (element.previlegeHMG ?? false); + } + } + return false; + } } diff --git a/lib/ui/landing/itg/change_itg_ad_password_screen.dart b/lib/ui/landing/itg/change_itg_ad_password_screen.dart index cc84f96..a8d7650 100644 --- a/lib/ui/landing/itg/change_itg_ad_password_screen.dart +++ b/lib/ui/landing/itg/change_itg_ad_password_screen.dart @@ -41,7 +41,7 @@ class _ChangeItgAdPasswordScreenState extends State { GenericResponseModel response = await LoginApiClient().changePasswordFromActiveDirectorySession(password.text, AppState().memberInformationList!.eMPLOYEEEMAILADDRESS!); Utils.hideLoading(context); if ((response.messageStatus ?? 0) == 1) { - Utils.showToast(LocaleKeys.passwordChangedSuccessfully.tr()); + Utils.showToast(LocaleKeys.passwordChangedSuccessfully.tr(), isSuccess: true); Navigator.pop(context); } } catch (ex) { diff --git a/lib/ui/landing/itg/survey_screen.dart b/lib/ui/landing/itg/survey_screen.dart index 3bbb27f..20a2bb9 100644 --- a/lib/ui/landing/itg/survey_screen.dart +++ b/lib/ui/landing/itg/survey_screen.dart @@ -222,7 +222,7 @@ class _SurveyScreenState extends State { Utils.hideLoading(context); if (res!.mohemmItgResponseItem!.statusCode == 200) { - Utils.showToast("Survey has been submitted successfully"); + Utils.showToast("Survey has been submitted successfully", isSuccess: true); Navigator.pop(context); } else { Utils.showToast(res.mohemmItgResponseItem!.message.toString()); diff --git a/lib/ui/landing/widget/app_drawer.dart b/lib/ui/landing/widget/app_drawer.dart index b9bdbc4..3f9b2e4 100644 --- a/lib/ui/landing/widget/app_drawer.dart +++ b/lib/ui/landing/widget/app_drawer.dart @@ -2,6 +2,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_svg/flutter_svg.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/utils.dart'; @@ -11,6 +12,7 @@ import 'package:mohem_flutter_app/extensions/string_extensions.dart'; import 'package:mohem_flutter_app/extensions/widget_extensions.dart'; import 'package:mohem_flutter_app/generated/locale_keys.g.dart'; import 'package:mohem_flutter_app/models/dashboard/drawer_menu_item_model.dart'; +import 'package:mohem_flutter_app/models/generic_response_model.dart'; import 'package:mohem_flutter_app/provider/chat_provider_model.dart'; import 'package:mohem_flutter_app/provider/dashboard_provider_model.dart'; import 'package:mohem_flutter_app/ui/dialogs/id/business_card_dialog.dart'; @@ -37,6 +39,22 @@ class _AppDrawerState extends State { chatData = Provider.of(context, listen: false); } + Future _showBusinessCard(BuildContext context) async { + Utils.showLoading(context); + try { + GenericResponseModel? response = await ProfileApiClient().getEmployeeBusinessCardInfo(); + Utils.hideLoading(context); + showMDialog( + context, + child: BusinessCardDialog(qrBase64: response?.businessCardQROutside, employeeInfo: response?.empBusinessCardInfoModel), + isBusniessCard: true, + ); + } catch (ex) { + Utils.hideLoading(context); + Utils.handleException(ex, context, null); + } + } + @override Widget build(BuildContext context) { if (drawerMenuItemList.isEmpty) { @@ -119,7 +137,7 @@ class _AppDrawerState extends State { }), menuItem("assets/images/drawer/employee_id.svg", LocaleKeys.employeeDigitalID.tr(), "", closeDrawer: false, onPress: () => showMDialog(context, child: EmployeeDigitialIdDialog())), if (AppState().businessCardPrivilege) - menuItem("assets/images/drawer/view_business_card.svg", LocaleKeys.viewBusinessCard.tr(), "", closeDrawer: false, onPress: () => showMDialog(context, child: BusinessCardDialog(), isBusniessCard: true)), + menuItem("assets/images/drawer/view_business_card.svg", LocaleKeys.viewBusinessCard.tr(), "", closeDrawer: false, onPress: () => _showBusinessCard(context)), menuItem("assets/images/drawer/logout.svg", LocaleKeys.logout.tr(), "", color: MyColors.redA3Color, closeDrawer: false, onPress: (){Utils.performLogout(context, chatData);}), // menuItem("assets/images/drawer/logout.svg", LocaleKeys.logout.tr(), "", color: MyColors.redA3Color, closeDrawer: false, onPress: () {Navigator.pushNamed(context, AppRoutes.survey,); ], diff --git a/lib/ui/landing/widget/services_widget.dart b/lib/ui/landing/widget/services_widget.dart index f1e03f8..74cfc0f 100644 --- a/lib/ui/landing/widget/services_widget.dart +++ b/lib/ui/landing/widget/services_widget.dart @@ -111,7 +111,7 @@ class ServicesWidget extends StatelessWidget { Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ - Expanded(child: data.homeMenus![parentIndex].menuEntiesList[index].prompt!.toText11(isBold: true)), + Expanded(child: data.homeMenus![parentIndex].menuEntiesList[index].prompt!.toText11(isBold: true, maxLine: 3)), RotatedBox(quarterTurns: AppState().isArabic(context) ? 2 : 4, child: SvgPicture.asset("assets/images/arrow_next.svg").paddingOnly(bottom: 4)), ], ), diff --git a/lib/ui/login/change_password_screen.dart b/lib/ui/login/change_password_screen.dart index e96901b..c7591d5 100644 --- a/lib/ui/login/change_password_screen.dart +++ b/lib/ui/login/change_password_screen.dart @@ -39,7 +39,7 @@ class _ChangePasswordScreenState extends State { try { var genericResponseModel = await LoginApiClient().changePasswordFromActiveSession(oldPassword.text, password.text, confirmPassword.text); Utils.hideLoading(context); - Utils.showToast(LocaleKeys.passwordChangedSuccessfully.tr()); + Utils.showToast(LocaleKeys.passwordChangedSuccessfully.tr(), isSuccess: true); Navigator.pop(context); } catch (ex) { Utils.hideLoading(context); diff --git a/lib/ui/login/forgot_password_screen.dart b/lib/ui/login/forgot_password_screen.dart index b2d5c92..d95ef20 100644 --- a/lib/ui/login/forgot_password_screen.dart +++ b/lib/ui/login/forgot_password_screen.dart @@ -50,29 +50,27 @@ class _ForgotPasswordScreenState extends State { context, 1, int.tryParse(_basicMemberInformation?.pMOBILENUMBER ?? ""), - (value,TextEditingController _pinPutController) async { + (value, TextEditingController _pinPutController) async { Utils.showLoading(context); - try{ - GenericResponseModel? genericResponseModel = await LoginApiClient().checkPublicActivationCode(value, employeeId.text); - if (genericResponseModel?.errorMessage != null) { - Utils.showToast(genericResponseModel?.errorMessage ?? ""); - return; - } - Utils.hideLoading(context); - await Navigator.pushNamed(context, AppRoutes.newPassword, arguments: employeeId.text); - Navigator.pop(context); - Navigator.pop(context); - }catch(ex){ - print(ex); - _pinPutController.clear(); - otpFieldClear.value = ""; - Utils.hideLoading(context); - Utils.handleException(ex, context, null); - } - }, - () => { - Navigator.pop(context), + try { + GenericResponseModel? genericResponseModel = await LoginApiClient().checkPublicActivationCode(value, employeeId.text); + if (genericResponseModel?.errorMessage != null) { + Utils.showToast(genericResponseModel?.errorMessage ?? ""); + return; + } + Utils.hideLoading(context); + await Navigator.pushNamed(context, AppRoutes.newPassword, arguments: employeeId.text); + Navigator.pop(context); + Navigator.pop(context); + } catch (ex) { + print(ex); + _pinPutController.clear(); + otpFieldClear.value = ""; + Utils.hideLoading(context); + Utils.handleException(ex, context, null); + } }, + () => {Navigator.pop(context)}, onResendCode: () { performForgotPassword(); }, @@ -87,13 +85,7 @@ class _ForgotPasswordScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar( - backgroundColor: Colors.transparent, - leading: IconButton( - icon: const Icon(Icons.arrow_back_ios, color: MyColors.darkIconColor), - onPressed: () => Navigator.pop(context), - ), - ), + appBar: AppBar(backgroundColor: Colors.transparent, leading: IconButton(icon: const Icon(Icons.arrow_back_ios, color: MyColors.darkIconColor), onPressed: () => Navigator.pop(context))), body: Column( children: [ //const SizedBox(height: 23), @@ -122,19 +114,19 @@ class _ForgotPasswordScreenState extends State { ), ], ), - ) + ), ], ), ), ), DefaultButton( - LocaleKeys.changePassword.tr(), - employeeId.text.isEmpty - ? null - : () async { - performForgotPassword(); - }) - .insideContainer + LocaleKeys.changePassword.tr(), + employeeId.text.isEmpty + ? null + : () async { + performForgotPassword(); + }, + ).insideContainer, ], ), ); diff --git a/lib/ui/login/login_screen.dart b/lib/ui/login/login_screen.dart index 13d8853..f6cb4ab 100644 --- a/lib/ui/login/login_screen.dart +++ b/lib/ui/login/login_screen.dart @@ -1,13 +1,14 @@ 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'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_svg/flutter_svg.dart'; // import 'package:huawei_hmsavailability/huawei_hmsavailability.dart'; import 'package:mohem_flutter_app/api/login_api_client.dart'; @@ -18,14 +19,17 @@ import 'package:mohem_flutter_app/classes/notifications.dart'; import 'package:mohem_flutter_app/classes/utils.dart'; import 'package:mohem_flutter_app/config/routes.dart'; import 'package:mohem_flutter_app/dialogs/otp_dialog.dart'; +import 'package:mohem_flutter_app/widgets/bottom_sheets/confirm_bottom_sheet.dart'; import 'package:mohem_flutter_app/extensions/int_extensions.dart'; import 'package:mohem_flutter_app/extensions/string_extensions.dart'; import 'package:mohem_flutter_app/extensions/widget_extensions.dart'; import 'package:mohem_flutter_app/generated/locale_keys.g.dart'; +import 'package:mohem_flutter_app/ui/profile/mazaya_access_result_screen.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/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'; @@ -48,7 +52,11 @@ class LoginScreen extends StatefulWidget { class _LoginScreenState extends State { TextEditingController username = TextEditingController(); TextEditingController password = TextEditingController(); + TextEditingController nationalId = TextEditingController(); + TextEditingController dobController = TextEditingController(); + DateTime? selectedDob; + // ignore: unused_field CheckMobileAppVersionModel? _checkMobileAppVersion; MemberLoginListModel? _memberLoginList; @@ -70,6 +78,11 @@ class _LoginScreenState extends State { @override void initState() { super.initState(); + ApiConsts.selectedMemberType = MemberType.employee; + ApiConsts.ISDependetLogin = false; + nationalId.addListener(_onCredentialsChanged); + dobController.addListener(_onCredentialsChanged); + checkDependentPrefs(); // hmsApiAvailability = HmsApiAvailability(); // checkFirebaseToken(); if (kReleaseMode) { @@ -77,6 +90,12 @@ class _LoginScreenState extends State { } } + void _onCredentialsChanged() { + if (mounted && ApiConsts.ISDependetLogin) { + setState(() {}); + } + } + void checkDeviceSafety() { try { SafeDevice.isJailBroken.then((bool value) { @@ -99,11 +118,15 @@ class _LoginScreenState extends State { @override void dispose() { + nationalId.removeListener(_onCredentialsChanged); + dobController.removeListener(_onCredentialsChanged); + dobController.dispose(); super.dispose(); } String? firebaseToken; GetMobileLoginInfoListModel? loginInfo; + GetMobileLoginInfoDependentModel? dependentLoginInfo; Future checkFirebaseToken() async { try { @@ -122,6 +145,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 = ""; } @@ -142,6 +169,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"); @@ -158,7 +189,84 @@ class _LoginScreenState extends State { void checkLoginInfo() async { try { - loginInfo = await LoginApiClient().getMobileLoginInfoNEW(AppState().getIsHuawei ? AppState().getHuaweiPushToken : firebaseToken ?? "", Platform.isAndroid ? "android" : "ios"); + 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); + + bool isEmpLast = AppState().isEmpLastLoginToDevice ?? true; + + if (!isEmpLast) { + // Dependent login flow + ApiConsts.selectedMemberType = MemberType.dependent; + ApiConsts.ISDependetLogin = true; + if (mounted) { + setState(() { + ApiConsts.selectedMemberType = MemberType.dependent; + ApiConsts.ISDependetLogin = true; + }); + } + + bool autoLoggedIn = await _tryDependentAutoLogin(); + if (autoLoggedIn) { + return; + } else { + dependentLoginInfo = await LoginApiClient().getMobileLoginInfoNEWDependent(devToken, devType, memberType: MemberType.dependent.value); + Utils.hideLoading(context); + if (dependentLoginInfo != null) { + _handleDependentLoginInfo(dependentLoginInfo!); + } + if (mounted) { + setState(() {}); + } + return; + } + } + + // Employee login flow + ApiConsts.selectedMemberType = MemberType.employee; + ApiConsts.ISDependetLogin = false; + if (mounted) { + setState(() { + ApiConsts.selectedMemberType = MemberType.employee; + ApiConsts.ISDependetLogin = false; + }); + } + if (loginInfo == null) { await checkPrefs(); _autoLogin = false; @@ -175,6 +283,98 @@ 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('dd-MM-yyyy').format(parsed); + } else { + dobController.text = rawDob; + } + } + + if (info.dependentName != null && info.dependentName!.isNotEmpty) { + String cleanedName = info.dependentName!.replaceAll(RegExp(r'\s*-\s*'), ' ').replaceAll(RegExp(r'\s+'), ' ').trim(); + AppState().setDependentName = cleanedName; + Utils.saveStringFromPrefs("P_DEPENDENT_NAME", cleanedName); + } + } + + 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 = ['dd-MM-yyyy', 'yyyy-MM-dd', 'dd/MM/yyyy', 'MM/dd/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('dd-MM-yyyy').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('dd-MM-yyyy').format(parsed); + } + } + } + Future checkPrefs() async { String username = await Utils.getStringFromPrefs(SharedPrefsConsts.username); if (username.isNotEmpty) { @@ -204,10 +404,26 @@ class _LoginScreenState extends State { AppState().setMohemmWifiPassword = mohemmWifiPassword; } Utils.hideLoading(context); - if (_autoLogin) { + if (firebaseToken == null || firebaseToken!.isEmpty || firebaseToken == "null") { + if (AppState().getDeviceToken != null && AppState().getDeviceToken!.isNotEmpty && AppState().getDeviceToken != "null") { + firebaseToken = AppState().getDeviceToken; + } else { + String saved = await Utils.getStringFromPrefs("P_DEP_DEVICE_TOKEN"); + if (saved.isNotEmpty && saved != "null") { + firebaseToken = saved; + } + } + } + String effectiveTokenArg = (firebaseToken != null && firebaseToken!.isNotEmpty && firebaseToken != "null") + ? firebaseToken! + : (AppState().getDeviceToken ?? ""); + if (_autoLogin && loginInfo != null) { + if (loginInfo!.deviceToken == null || loginInfo!.deviceToken!.isEmpty || loginInfo!.deviceToken == "null") { + loginInfo!.deviceToken = effectiveTokenArg; + } Navigator.pushReplacementNamed(context, AppRoutes.verifyLastLogin, arguments: loginInfo); } else { - Navigator.pushNamed(context, AppRoutes.verifyLogin, arguments: "$firebaseToken"); + Navigator.pushNamed(context, AppRoutes.verifyLogin, arguments: effectiveTokenArg); } Utils.saveStringFromPrefs(SharedPrefsConsts.password, password.text); } catch (ex) { @@ -215,19 +431,21 @@ class _LoginScreenState extends State { print(ex.toString()); Utils.hideLoading(context); Utils.handleException(ex, context, (msg) { - Utils.confirmDialog( - context, - msg, - onTap: () async { - if (msg.toLowerCase().contains("password has expired")) { - Navigator.pop(context); - // await Navigator.pushNamed(context, AppRoutes.newPassword, arguments: username.text); + if (msg.toLowerCase().contains("password has expired")) { + // Sends an OTP/activation code via performForgotPassword() - requires an explicit + // tap, not an auto-firing toast, since it's a real side effect, not just navigation. + ConfirmBottomSheet.show( + context, + message: msg, + confirmText: LocaleKeys.forgotPassword.tr(), + showCancelButton: false, + onConfirm: () { performForgotPassword(); - } else { - Navigator.pop(context); - } - }, - ); + }, + ); + } else { + Utils.showToast(msg); + } }); } } @@ -265,9 +483,7 @@ class _LoginScreenState extends State { Utils.handleException(ex, context, null); } }, - () => { - Navigator.pop(context), - }, + () => {Navigator.pop(context)}, onResendCode: () { performForgotPassword(); }, @@ -279,6 +495,529 @@ class _LoginScreenState extends State { } } + void _switchToMode(MemberType type) async { + if (ApiConsts.selectedMemberType != type) { + AppState().setIsEmpLastLoginToDevice = (type == MemberType.employee); + setState(() { + ApiConsts.selectedMemberType = type; + ApiConsts.ISDependetLogin = (type == MemberType.dependent); + }); + if (type == MemberType.dependent) { + await checkDependentPrefs(); + } else { + await checkPrefs(); + } + if (firebaseToken == null || firebaseToken!.isEmpty) { + String saved = await Utils.getStringFromPrefs("P_DEP_DEVICE_TOKEN"); + if (saved.isNotEmpty) { + firebaseToken = saved; + } + } + } + } + + Widget _buildCtaCard({bool isSVG = false, required String iconPath, required BuildContext context, 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.start, + children: [ + Container( + width: 40, + height: 40, + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(10)), + alignment: Alignment.center, + child: isSVG ? SvgPicture.asset(iconPath, height: 22, width: 22).paddingOnly(top: 2, left: 3, right: 3) : Image.asset(iconPath, height: 22, width: 22).paddingOnly(top: 2), + ), + 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)), + ], + ), + ), + 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, + isSVG: false, + iconPath: 'assets/images/mazaya.png', + title: LocaleKeys.joinMazaya.tr(), + subtitle: LocaleKeys.joinMazayaSubtitle.tr(), + onTap: () => _switchToMode(MemberType.dependent), + ); + } + + Widget _buildEmployeeCtaCard(BuildContext context) { + return _buildCtaCard( + context: context, + iconPath: 'assets/images/mohem.png', + 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); + DateTime lastDate = DateTime.now(); + + DateTime? picked = await showDatePicker( + context: context, + initialDate: initialDate.isAfter(lastDate) ? lastDate : initialDate, + firstDate: firstDate, + lastDate: lastDate, + builder: (context, child) { + return Theme(data: Theme.of(context).copyWith(colorScheme: const ColorScheme.light(primary: MyColors.textMixColor, onPrimary: Colors.white, onSurface: MyColors.darkTextColor)), child: child!); + }, + ); + + if (picked != null) { + setState(() { + selectedDob = picked; + dobController.text = DateFormat('dd-MM-yyyy', 'en_US').format(picked); + }); + } + } + + String getFormattedBirthDate() { + DateTime? date = selectedDob ?? _parseBirthDate(dobController.text.trim()); + if (date != null) { + int utcMillis = DateTime.utc(date.year, date.month, date.day).millisecondsSinceEpoch; + return "/Date($utcMillis)/"; + } + return ""; + } + + void _navigateToMazayaAccessRevoked([String? depName, String? message]) async { + await Utils.removeStringFromPrefs("P_DEP_IS_LOGGED_IN"); + await Utils.removeStringFromPrefs("P_DEP_NATIONAL_ID"); + await Utils.removeStringFromPrefs("P_DEP_BIRTH_DATE"); + await Utils.removeStringFromPrefs("P_DEP_TOKEN_ID"); + await Utils.removeStringFromPrefs("P_DEP_LOGIN_TOKEN_ID"); + await Utils.removeStringFromPrefs("P_DEP_AUTHENTICATION_TOKEN_ID"); + await Utils.removeStringFromPrefs("P_DEP_MOBILE_NUMBER"); + await Utils.removeStringFromPrefs("P_DEPENDENT_NAME"); + await Utils.removeStringFromPrefs("P_DEP_LOGIN_TYPE"); + ApiConsts.selectedMemberType = MemberType.employee; + ApiConsts.ISDependetLogin = false; + AppState().setDependentName = null; + AppState().setDependentDob = null; + AppState().setDependentLoginType = null; + AppState().setWalaplusDepInfoModel = null; + AppState().setPostParamsInitConfig(); + + String name = depName ?? AppState().dependentName ?? ""; + if (name.isEmpty) { + name = await Utils.getStringFromPrefs("P_DEPENDENT_NAME"); + } + if (!mounted) return; + Navigator.push(context, MaterialPageRoute(builder: (_) => MazayaAccessResultScreen(isGranted: false, dependentName: name, message: message))); + } + + void performDependentLogin() async { + if (nationalId.text.trim().isEmpty) { + Utils.showToast(LocaleKeys.pleaseEnterNationalId.tr()); + return; + } + String birthDateStr = getFormattedBirthDate(); + if (birthDateStr.isEmpty) { + 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, + ); + + if (depLoginResponse?.errorMessage != null && depLoginResponse!.errorMessage!.isNotEmpty) { + Utils.hideLoading(context); + if (Utils.isMazayaAccessRevoked(depLoginResponse.errorMessage)) { + _navigateToMazayaAccessRevoked(); + return; + } + Utils.showToast(depLoginResponse.errorMessage!); + return; + } + + if (depLoginResponse?.getWalaplusDepInfoModel != null) { + AppState().setWalaplusDepInfoModel = depLoginResponse!.getWalaplusDepInfoModel; + String? depName = depLoginResponse.getWalaplusDepInfoModel?.pDependentName; + if (depName != null && depName.trim().isNotEmpty) { + String cleanedName = depName.replaceAll(RegExp(r'\s*-\s*'), ' ').replaceAll(RegExp(r'\s+'), ' ').trim(); + AppState().setDependentName = cleanedName; + Utils.saveStringFromPrefs("P_DEPENDENT_NAME", cleanedName); + } + } + + String? authToken = (depLoginResponse?.authenticationTokenID != null && depLoginResponse!.authenticationTokenID!.trim().isNotEmpty) ? depLoginResponse.authenticationTokenID!.trim() : null; + + String? logInTokenID = depLoginResponse?.logInTokenID; + String? mobileNumber = depLoginResponse?.getWalaplusDepInfoModel?.pEmpMobileNumber; + + // 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(LocaleKeys.unableToRetrieveDependentInfo.tr()); + return; + } + + GenericResponseModel? sendOtpResponse = await LoginApiClient().mohemmSendOTPDependent( + nationalId: nationalId.text.trim(), + mobileNumber: mobileNumber, + logInTokenID: logInTokenID, + context: context, + ); + + Utils.hideLoading(context); + + if (sendOtpResponse?.messageStatus == 1) { + // Step 3: Open OTP Dialog + _showDependentOtpDialog(logInTokenID, mobileNumber); + } else { + Utils.showToast(sendOtpResponse?.errorMessage ?? LocaleKeys.failedToSendOtp.tr()); + } + } catch (ex) { + if (kDebugMode) print(ex); + Utils.hideLoading(context); + if (Utils.isMazayaAccessRevoked(ex)) { + _navigateToMazayaAccessRevoked(); + return; + } + Utils.handleException(ex, context, null); + } + } + + void _showDependentOtpDialog(String logInTokenID, String mobileNumber) { + OtpDialog( + context, + 1, + int.tryParse(mobileNumber), + (value, TextEditingController _pinPutController) async { + Utils.showLoading(context); + try { + GenericResponseModel? checkOtpResponse = await LoginApiClient().mohemmCheckOTPDependent( + logInTokenID: logInTokenID, + activationCode: value, + nationalId: nationalId.text.trim(), + mobileNumber: mobileNumber, + context: context, + ); + Utils.hideLoading(context); + + if (checkOtpResponse?.errorMessage != null && checkOtpResponse!.errorMessage!.isNotEmpty) { + if (Utils.isMazayaAccessRevoked(checkOtpResponse.errorMessage)) { + Navigator.pop(context); + _navigateToMazayaAccessRevoked(); + return; + } + Utils.showToast(checkOtpResponse.errorMessage!); + _pinPutController.clear(); + otpFieldClear.value = ""; + return; + } + + Navigator.pop(context); + + _completeDependentLogin(checkOtpResponse, logInTokenID, mobileNumber, loginType: 1, navigateToDashboard: true); + } catch (ex) { + if (kDebugMode) print(ex); + _pinPutController.clear(); + otpFieldClear.value = ""; + Utils.hideLoading(context); + if (Utils.isMazayaAccessRevoked(ex)) { + Navigator.pop(context); + _navigateToMazayaAccessRevoked(); + return; + } + Utils.handleException(ex, context, null); + } + }, + () { + Navigator.pop(context); + }, + onResendCode: () async { + Utils.showLoading(context); + try { + await LoginApiClient().mohemmSendOTPDependent(nationalId: nationalId.text.trim(), mobileNumber: mobileNumber, logInTokenID: logInTokenID, context: context); + Utils.hideLoading(context); + Utils.showToast(LocaleKeys.verificationCodeResent.tr(), isSuccess: true); + _showDependentOtpDialog(logInTokenID, mobileNumber); + } catch (ex) { + Utils.hideLoading(context); + if (Utils.isMazayaAccessRevoked(ex)) { + Navigator.pop(context); + _navigateToMazayaAccessRevoked(); + return; + } + Utils.handleException(ex, context, null); + } + }, + ).displayDialog(context); + } + + bool _isAutoLoggingInDependent = false; + + Future _tryDependentAutoLogin() async { + if (_isAutoLoggingInDependent) return true; + _isAutoLoggingInDependent = true; + + try { + String isDepLoggedIn = await Utils.getStringFromPrefs("P_DEP_IS_LOGGED_IN"); + if (isDepLoggedIn != "true") { + return false; + } + String savedNid = await Utils.getStringFromPrefs("P_DEP_NATIONAL_ID"); + if (savedNid.isEmpty) { + return false; + } + String savedDob = await Utils.getStringFromPrefs("P_DEP_BIRTH_DATE"); + String savedToken = await Utils.getStringFromPrefs("P_DEP_TOKEN_ID"); + String savedLoginToken = await Utils.getStringFromPrefs("P_DEP_LOGIN_TOKEN_ID"); + String savedAuthToken = await Utils.getStringFromPrefs("P_DEP_AUTHENTICATION_TOKEN_ID"); + String savedMobile = await Utils.getStringFromPrefs("P_DEP_MOBILE_NUMBER"); + String savedName = await Utils.getStringFromPrefs("P_DEPENDENT_NAME"); + String savedLoginType = await Utils.getStringFromPrefs("P_DEP_LOGIN_TYPE"); + int loginType = int.tryParse(savedLoginType) ?? 1; + + String effectiveToken = savedAuthToken.isNotEmpty ? savedAuthToken : (savedToken.isNotEmpty ? savedToken : savedLoginToken); + + if (nationalId.text.isEmpty) { + nationalId.text = savedNid; + } + if (savedDob.isNotEmpty && dobController.text.isEmpty) { + DateTime? parsed = _parseBirthDate(savedDob); + if (parsed != null) { + selectedDob = parsed; + dobController.text = DateFormat('dd-MM-yyyy').format(parsed); + } else { + dobController.text = savedDob; + } + } + + if (AppState().postParamsObject == null) { + AppState().setPostParamsInitConfig(); + } + + ApiConsts.selectedMemberType = MemberType.dependent; + ApiConsts.ISDependetLogin = true; + + AppState().postParamsObject?.setTokenID = effectiveToken; + AppState().setAuthenticationTokenID = effectiveToken; + AppState().postParamsObject?.setLogInTokenID = savedLoginToken.isNotEmpty ? savedLoginToken : effectiveToken; + AppState().postParamsObject?.setUserName = savedNid; + AppState().postParamsObject?.setPUserName = savedNid; + if (savedMobile.isNotEmpty) { + AppState().postParamsObject?.setMobileNumer = savedMobile; + } + AppState().postParamsObject?.channel = 31; + AppState().postParamsObject?.versionID = 20; + AppState().setUserName = savedNid; + AppState().isAuthenticated = true; + AppState().setLogged = true; + if (savedName.isNotEmpty) { + AppState().setDependentName = savedName; + } + if (savedDob.isNotEmpty) { + AppState().setDependentDob = savedDob; + } + AppState().setDependentLoginType = loginType; + + if (savedDob.isNotEmpty) { + String devToken = AppState().getIsHuawei ? AppState().getHuaweiPushToken : (firebaseToken ?? ""); + if (devToken.isEmpty && !AppState().getIsHuawei) { + if (AppState().getDeviceToken != null && AppState().getDeviceToken!.isNotEmpty) { + devToken = AppState().getDeviceToken!; + } else { + devToken = await Utils.getStringFromPrefs("P_DEP_DEVICE_TOKEN"); + } + } + String devType = Platform.isAndroid ? "android" : "ios"; + + try { + GenericResponseModel? depLoginResponse = await LoginApiClient().memberLoginDependent(nationalId: savedNid, birthDate: savedDob, deviceToken: devToken, deviceType: devType, context: context); + + if (depLoginResponse?.errorMessage != null && depLoginResponse!.errorMessage!.isNotEmpty) { + if (Utils.isMazayaAccessRevoked(depLoginResponse.errorMessage)) { + if (mounted) { + Utils.hideLoading(context); + _navigateToMazayaAccessRevoked(savedName); + } + return true; + } + } + + if (depLoginResponse != null) { + String? authToken = (depLoginResponse.authenticationTokenID != null && depLoginResponse.authenticationTokenID!.trim().isNotEmpty) ? depLoginResponse.authenticationTokenID!.trim() : null; + String? logInTokenID = depLoginResponse.logInTokenID; + String? mobileNumber = depLoginResponse.getWalaplusDepInfoModel?.pEmpMobileNumber ?? savedMobile; + + if (authToken != null && authToken.isNotEmpty) { + if (mounted) { + Utils.hideLoading(context); + _completeDependentLogin(depLoginResponse, logInTokenID ?? authToken, mobileNumber, loginType: loginType, navigateToDashboard: true); + } + return true; + } + } + } catch (ex) { + if (Utils.isMazayaAccessRevoked(ex)) { + if (mounted) { + Utils.hideLoading(context); + _navigateToMazayaAccessRevoked(savedName); + } + return true; + } + if (kDebugMode) print("Error in memberLoginDependent auto-login: $ex"); + } + } + + if (effectiveToken.isNotEmpty && mounted) { + Utils.hideLoading(context); + Navigator.pushNamedAndRemoveUntil(context, AppRoutes.mazayaDiscover, (Route route) => false); + return true; + } + } catch (e) { + if (kDebugMode) print("Error in _tryDependentAutoLogin: $e"); + } finally { + _isAutoLoggingInDependent = false; + } + + return false; + } + + 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)); + + String effectiveNationalId = nationalId.text.trim().isNotEmpty ? nationalId.text.trim() : (AppState().getUserName ?? ""); + String effectiveDob = getFormattedBirthDate().isNotEmpty ? getFormattedBirthDate() : (AppState().dependentDob ?? ""); + + AppState().postParamsObject?.setTokenID = effectiveToken; + AppState().setAuthenticationTokenID = effectiveToken; + AppState().postParamsObject?.setLogInTokenID = (checkOtpResponse?.logInTokenID != null && checkOtpResponse!.logInTokenID!.isNotEmpty) ? checkOtpResponse.logInTokenID! : logInTokenID; + AppState().postParamsObject?.setUserName = effectiveNationalId; + AppState().postParamsObject?.setPUserName = effectiveNationalId; + + String resolvedMobile = mobileNumber.isNotEmpty ? mobileNumber : (checkOtpResponse?.getWalaplusDepInfoModel?.pEmpMobileNumber ?? ""); + if (resolvedMobile.isNotEmpty) { + AppState().postParamsObject?.setMobileNumer = resolvedMobile; + Utils.saveStringFromPrefs("P_DEP_MOBILE_NUMBER", resolvedMobile); + } + + AppState().setUserName = effectiveNationalId; + 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 = effectiveDob; + + Utils.saveStringFromPrefs("P_DEP_IS_LOGGED_IN", "true"); + Utils.saveStringFromPrefs("P_DEP_NATIONAL_ID", effectiveNationalId); + Utils.saveStringFromPrefs("P_DEP_BIRTH_DATE", effectiveDob); + 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()); + } + String tokenToSave = AppState().getIsHuawei + ? AppState().getHuaweiPushToken + : ((firebaseToken != null && firebaseToken!.isNotEmpty && firebaseToken != "null") + ? firebaseToken! + : (AppState().getDeviceToken ?? "")); + if (tokenToSave.isNotEmpty && tokenToSave != "null") { + Utils.saveStringFromPrefs("P_DEP_DEVICE_TOKEN", tokenToSave); + } + Utils.saveStringFromPrefs("P_DEP_LOGIN_TYPE", loginType.toString()); + + if (navigateToDashboard && ApiConsts.ISDependetLogin) { + Navigator.pushNamedAndRemoveUntil(context, AppRoutes.mazayaDiscover, (Route route) => false); + } + } + + void _ensureDeviceTokenLoaded() async { + if (firebaseToken == null || firebaseToken!.isEmpty || firebaseToken == "null") { + if (AppState().getDeviceToken != null && AppState().getDeviceToken!.isNotEmpty && AppState().getDeviceToken != "null") { + firebaseToken = AppState().getDeviceToken; + } else { + String saved = await Utils.getStringFromPrefs("P_DEP_DEVICE_TOKEN"); + if (saved.isNotEmpty && saved != "null") { + firebaseToken = saved; + AppState().setDeviceToken = saved; + } else if (!AppState().getIsHuawei) { + try { + await Firebase.initializeApp(); + String? tok = await FirebaseMessaging.instance.getToken(); + if (tok != null && tok.isNotEmpty) { + firebaseToken = tok; + AppState().setDeviceToken = tok; + Utils.saveStringFromPrefs("P_DEP_DEVICE_TOKEN", tok); + } + } catch (_) {} + } + } + } + } + @override Widget build(BuildContext context) { if (isAppOpenBySystem == null) { @@ -294,7 +1033,11 @@ class _LoginScreenState extends State { // 13777 // Ab12345cd } - if (isAppOpenBySystem!) checkFirebaseToken(); + if (isAppOpenBySystem!) { + checkFirebaseToken(); + } else { + _ensureDeviceTokenLoaded(); + } } // username.text = "15444"; @@ -311,68 +1054,90 @@ 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(() { context.setLocale(const Locale("en", "US")); + _ensureDeviceTokenLoaded(); }), - Container( - width: 1, - color: MyColors.darkWhiteColor, - height: 16, - margin: const EdgeInsets.only(left: 10, right: 10), - ), + Container(width: 1, color: MyColors.darkWhiteColor, height: 16, margin: const EdgeInsets.only(left: 10, right: 10)), LocaleKeys.arabic.tr().toText14(color: !AppState().isArabic(context) ? null : MyColors.textMixColor).onPress(() { context.setLocale(const Locale("ar", "SA")); + _ensureDeviceTokenLoaded(); }), ], ), ], ), Expanded( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - LocaleKeys.login.tr().toText24(isBold: true), - LocaleKeys.pleaseEnterLoginDetails.tr().toText16(), - 16.height, - 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), + 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.nationalIdOrIqamaNumber.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(), "DD-MM-YYYY", 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'); - performLogin(); - }).insideContainer + if (ApiConsts.ISDependetLogin) { + performDependentLogin(); + } else { + performLogin(); + } + }).insideContainer, ], ), ); diff --git a/lib/ui/login/new_password_screen.dart b/lib/ui/login/new_password_screen.dart index 70b7456..96ea18e 100644 --- a/lib/ui/login/new_password_screen.dart +++ b/lib/ui/login/new_password_screen.dart @@ -41,7 +41,7 @@ class _NewPasswordScreenState extends State { try { var genericResponseModel = await LoginApiClient().changePasswordForget(AppState().getForgetPasswordTokenID ?? "", password.text, confirmPassword.text, userName); Utils.hideLoading(context); - Utils.showToast(LocaleKeys.passwordChangedSuccessfully.tr()); + Utils.showToast(LocaleKeys.passwordChangedSuccessfully.tr(), isSuccess: true); Navigator.pop(context); } catch (ex) { print(ex); diff --git a/lib/ui/login/verify_last_login_screen.dart b/lib/ui/login/verify_last_login_screen.dart index 36425aa..ecb7ec2 100644 --- a/lib/ui/login/verify_last_login_screen.dart +++ b/lib/ui/login/verify_last_login_screen.dart @@ -8,8 +8,10 @@ import 'package:local_auth/local_auth.dart'; import 'package:local_auth_darwin/local_auth_darwin.dart'; import 'package:local_auth_android/local_auth_android.dart'; import 'package:mohem_flutter_app/api/login_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/date_uitl.dart'; import 'package:mohem_flutter_app/classes/utils.dart'; import 'package:mohem_flutter_app/config/routes.dart'; @@ -58,10 +60,9 @@ class _VerifyLastLoginScreenState extends State { @override Widget build(BuildContext context) { - if (ModalRoute.of(context)!.settings.arguments != null) { - mobileLoginInfoListModel ??= ModalRoute.of(context)!.settings.arguments as GetMobileLoginInfoListModel; - // String empName = AppState().isArabic(context) ? AppState().memberInformationList!.eMPLOYEEDISPLAYNAMEAr! : AppState().memberInformationList!.eMPLOYEEDISPLAYNAMEEn!; - String empName = mobileLoginInfoListModel!.employeeName!; + Object? args = ModalRoute.of(context)?.settings.arguments; + if (args is GetMobileLoginInfoListModel) { + mobileLoginInfoListModel ??= args; } return Scaffold( @@ -69,14 +70,22 @@ class _VerifyLastLoginScreenState extends State { surfaceTintColor: Colors.transparent, backgroundColor: Colors.transparent, automaticallyImplyLeading: false, - title: (mobileLoginInfoListModel?.businessCardPrivilege ?? false) - ? LocaleKeys.viewBusinessCard.tr().toText12(color: MyColors.textMixColor, isUnderLine: true).onPress(() { - showMDialog(context, child: const BusinessCardDialog()); - }) - : Container(), + title: + (mobileLoginInfoListModel?.businessCardPrivilege ?? false) + ? LocaleKeys.viewBusinessCard.tr().toText12(color: MyColors.textMixColor, isUnderLine: true).onPress(() async { + Utils.showLoading(context); + try { + GenericResponseModel? response = await ProfileApiClient().getEmployeeBusinessCardInfo(); + Utils.hideLoading(context); + showMDialog(context, child: BusinessCardDialog(qrBase64: response?.businessCardQROutside, employeeInfo: response?.empBusinessCardInfoModel)); + } catch (ex) { + Utils.hideLoading(context); + Utils.handleException(ex, context, null); + } + }) + : Container(), actions: [ Center( - child: LocaleKeys.employeeDigitalID.tr().toText12(color: MyColors.textMixColor, isUnderLine: true).onPress(() { showMDialog(context, child: EmployeeDigitialIdDialog()); }), @@ -112,16 +121,19 @@ class _VerifyLastLoginScreenState extends State { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.center, - children: [LocaleKeys.lastLoginDetails.tr().toText16(), DateUtil.formatDateToDate(DateUtil.convertStringToDate(mobileLoginInfoListModel?.editedOn ?? null), false).toText12()], + children: [ + LocaleKeys.lastLoginDetails.tr().toText16(), + mobileLoginInfoListModel != null ? DateUtil.formatDateToDate(DateUtil.convertStringToDate(mobileLoginInfoListModel?.editedOn ?? null), false).toText12() : const SizedBox(), + ], ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.center, children: [ LocaleKeys.verificationType.tr().toText10(color: MyColors.grey57Color), - getVerificationType(mobileLoginInfoListModel!.loginType!).toText12(), + getVerificationType(mobileLoginInfoListModel?.loginType).toText12(), const Expanded(child: SizedBox()), - DateUtil.formatDateToTime(DateUtil.convertStringToDate(mobileLoginInfoListModel?.editedOn)).toText12(), + DateUtil.formatDateToTime(DateUtil.convertStringToDate(mobileLoginInfoListModel?.editedOn ?? DateTime.now().toString())).toText12(), ], ), ], @@ -193,7 +205,7 @@ class _VerifyLastLoginScreenState extends State { if (mounted) setState(() {}); } - String getVerificationType(int type) { + String getVerificationType(int? type) { if (type == 1) { return LocaleKeys.sms.tr(); } else if (type == 2) { @@ -207,8 +219,12 @@ class _VerifyLastLoginScreenState extends State { } Future loginWithFaceIDAndBiometrics() async { - - IOSAuthMessages iosStrings = const IOSAuthMessages(cancelButton: 'cancel'); + IOSAuthMessages iosStrings = const IOSAuthMessages( + cancelButton: 'cancel', + // goToSettingsButton: 'settings', + // goToSettingsDescription: 'Please set up your Touch ID.', + // lockOut: 'Please reenable your Touch ID', + ); // IOSAuthMessages iosStrings = const IOSAuthMessages( // cancelButton: 'cancel', @@ -219,11 +235,12 @@ class _VerifyLastLoginScreenState extends State { bool authenticated = false; try { - authenticated = await auth.authenticate(localizedReason: 'Scan your fingerprint to authenticate', - authMessages: [ - iosStrings, - const AndroidAuthMessages(), - ], + authenticated = await auth.authenticate( + localizedReason: 'Scan your fingerprint to authenticate', + // options: const AuthenticationOptions(useErrorDialogs: true, stickyAuth: true, biometricOnly: true), + persistAcrossBackgrounding: false, // Replaces stickyAuth + biometricOnly: true, + authMessages: [iosStrings, const AndroidAuthMessages()], ); } on PlatformException catch (e) { print(e); @@ -234,60 +251,58 @@ class _VerifyLastLoginScreenState extends State { } Widget _loginOptionButton(String _title, String _icon, int _flag, int? _loginIndex) { -// <<<<<<< HEAD + // <<<<<<< HEAD bool isDisable = false; - if(_flag >= 3 && _flag <= 4) { - bool isFaceEnabled = (_flag == 3 && (checkBiometricIsAvailable(BiometricType.face) || - checkBiometricIsAvailable(BiometricType.weak))); + if (_flag >= 3 && _flag <= 4) { + bool isFaceEnabled = (_flag == 3 && (checkBiometricIsAvailable(BiometricType.face) || checkBiometricIsAvailable(BiometricType.weak))); - bool isThumbEnabled = - (_flag == 4 && (checkBiometricIsAvailable(BiometricType.fingerprint) || - checkBiometricIsAvailable(BiometricType.strong))); + bool isThumbEnabled = (_flag == 4 && (checkBiometricIsAvailable(BiometricType.fingerprint) || checkBiometricIsAvailable(BiometricType.strong))); isDisable = !(isFaceEnabled || isThumbEnabled); } -// ======= -// bool isDisable = (_flag == 3 && !checkBiometricIsAvailable(BiometricType.face) || -// _flag == 4 && !checkBiometricIsAvailable(BiometricType.fingerprint) && _flag == 4 && !checkBiometricIsAvailable(BiometricType.face)); -// // >>>>>>> master + // ======= + // bool isDisable = (_flag == 3 && !checkBiometricIsAvailable(BiometricType.face) || + // _flag == 4 && !checkBiometricIsAvailable(BiometricType.fingerprint) && _flag == 4 && !checkBiometricIsAvailable(BiometricType.face)); + // // >>>>>>> master return InkWell( - onTap: isDisable - ? null - : () async { - if (_flag == 0) { - setState(() { - // isMoreOption = true; - }); - } else { - if (_flag == 3 || _flag == 4) { - bool authenticateWithFaceAndTouchID = await loginWithFaceIDAndBiometrics(); - if (!authenticateWithFaceAndTouchID) { - return; - } else { - if (mobileLoginInfoListModel!.loginType == 3 || mobileLoginInfoListModel!.loginType == 4) { - // bool authenticateWithFaceAndTouchID = await loginWithFaceIDAndBiometrics(); - // if (!authenticateWithFaceAndTouchID) { - // return; - // } else { - // performApiCall(_title, _icon, _flag, isDirectLogin: true); - // } - performApiCall(_title, _icon, _flag, _flag, isDirectLogin: true); + onTap: + isDisable + ? null + : () async { + if (_flag == 0) { + setState(() { + // isMoreOption = true; + }); + } else { + if (_flag == 3 || _flag == 4) { + bool authenticateWithFaceAndTouchID = await loginWithFaceIDAndBiometrics(); + if (!authenticateWithFaceAndTouchID) { + return; } else { - isNeedVerifyWithFaceIDAndBiometrics = true; - selectedFlag = _flag; - setState(() { - return; - }); + if (mobileLoginInfoListModel?.loginType == 3 || mobileLoginInfoListModel?.loginType == 4) { + // bool authenticateWithFaceAndTouchID = await loginWithFaceIDAndBiometrics(); + // if (!authenticateWithFaceAndTouchID) { + // return; + // } else { + // performApiCall(_title, _icon, _flag, isDirectLogin: true); + // } + performApiCall(_title, _icon, _flag, _flag, isDirectLogin: true); + } else { + isNeedVerifyWithFaceIDAndBiometrics = true; + selectedFlag = _flag; + setState(() { + return; + }); + } } + } else { + if (isNeedVerifyWithFaceIDAndBiometrics) + performApiCall(_title, _icon, selectedFlag, _flag); + else + performApiCall(_title, _icon, _flag, _flag); } - } else { - if (isNeedVerifyWithFaceIDAndBiometrics) - performApiCall(_title, _icon, selectedFlag, _flag); - else - performApiCall(_title, _icon, _flag, _flag); } - } - }, + }, child: Container( padding: const EdgeInsets.only(left: 20, right: 20, bottom: 15, top: 28), decoration: BoxDecoration( @@ -374,7 +389,12 @@ class _VerifyLastLoginScreenState extends State { Future performDirectApiCall(String _title, String _icon, int _flag, String value, TextEditingController? _pinPutController, {bool isDirectLogin = false}) async { try { - GenericResponseModel? genericResponseModel = await LoginApiClient().checkActivationCode(true, AppState().memberLoginList?.pMOBILENUMBER, value, AppState().getUserName); + GenericResponseModel? genericResponseModel = await LoginApiClient().checkActivationCode(true, AppState().memberLoginList?.pMOBILENUMBER, value, AppState().getUserName,_flag.toString()); + String devToken = AppState().getIsHuawei + ? AppState().getHuaweiPushToken + : ((mobileLoginInfoListModel?.deviceToken != null && mobileLoginInfoListModel!.deviceToken!.isNotEmpty && mobileLoginInfoListModel!.deviceToken != "null") + ? mobileLoginInfoListModel!.deviceToken! + : (AppState().getDeviceToken ?? "")); GenericResponseModel? genericResponseModel1 = await LoginApiClient().insertMobileLoginInfoNEW( AppState().memberLoginList?.pEMAILADDRESS ?? "", genericResponseModel?.pSESSIONID ?? 0, @@ -382,8 +402,9 @@ class _VerifyLastLoginScreenState extends State { _flag, AppState().memberLoginList?.pMOBILENUMBER ?? "", AppState().getUserName!, - AppState().getIsHuawei ? AppState().getHuaweiPushToken : mobileLoginInfoListModel!.deviceToken!, + devToken, Platform.isAndroid ? "android" : "ios", + memberType: MemberType.employee.value, ); AppState().setMemberInformationListModel = genericResponseModel!.memberInformationList?.first; AppState().setPrivilegeListModel = genericResponseModel!.privilegeList ?? []; @@ -411,15 +432,15 @@ class _VerifyLastLoginScreenState extends State { } } -// -// formatDate(date) { -// return date; -// return DateFormat('MMM dd, yyy, kk:mm').format(date); -// } -// -// showLoader(bool isTrue) { -// setState(() { -// // isLoading = isTrue; -// }); -// } + // + // formatDate(date) { + // return date; + // return DateFormat('MMM dd, yyy, kk:mm').format(date); + // } + // + // showLoader(bool isTrue) { + // setState(() { + // // isLoading = isTrue; + // }); + // } } diff --git a/lib/ui/login/verify_login_screen.dart b/lib/ui/login/verify_login_screen.dart index 625db60..b6ea56b 100644 --- a/lib/ui/login/verify_login_screen.dart +++ b/lib/ui/login/verify_login_screen.dart @@ -55,7 +55,18 @@ class _VerifyLoginScreenState extends State { @override Widget build(BuildContext context) { - firebaseToken ??= ModalRoute.of(context)!.settings.arguments as String; + Object? rawArg = ModalRoute.of(context)?.settings.arguments; + if (rawArg != null) { + String argStr = rawArg.toString().trim(); + if (argStr.isNotEmpty && argStr != "null") { + firebaseToken ??= argStr; + } + } + if (firebaseToken == null || firebaseToken!.isEmpty || firebaseToken == "null") { + if (AppState().getDeviceToken != null && AppState().getDeviceToken!.isNotEmpty && AppState().getDeviceToken != "null") { + firebaseToken = AppState().getDeviceToken; + } + } return Scaffold( appBar: AppBar( backgroundColor: Colors.transparent, @@ -641,7 +652,12 @@ class _VerifyLoginScreenState extends State { (value, TextEditingController _pinPutController) async { Utils.showLoading(context); try { - GenericResponseModel? genericResponseModel = await LoginApiClient().checkActivationCode(true, AppState().memberLoginList?.pMOBILENUMBER, value, AppState().getUserName); + GenericResponseModel? genericResponseModel = await LoginApiClient().checkActivationCode(true, AppState().memberLoginList?.pMOBILENUMBER, value, AppState().getUserName, _flag.toString()); + String devToken = AppState().getIsHuawei + ? AppState().getHuaweiPushToken + : ((firebaseToken != null && firebaseToken!.isNotEmpty && firebaseToken != "null") + ? firebaseToken! + : (AppState().getDeviceToken ?? "")); GenericResponseModel? genericResponseModel1 = await LoginApiClient().insertMobileLoginInfoNEW( AppState().memberLoginList?.pEMAILADDRESS ?? "", genericResponseModel?.pSESSIONID ?? 0, @@ -649,8 +665,9 @@ class _VerifyLoginScreenState extends State { _flag, AppState().memberLoginList?.pMOBILENUMBER ?? "", AppState().getUserName!, - AppState().getIsHuawei ? AppState().getHuaweiPushToken : firebaseToken!, - Platform.isAndroid ? "android" : "ios"); + devToken, + Platform.isAndroid ? "android" : "ios", + memberType: MemberType.employee.value); if (genericResponseModel?.errorMessage != null) { Utils.showToast(genericResponseModel?.errorMessage ?? ""); } else { diff --git a/lib/ui/marathon/marathon_screen.dart b/lib/ui/marathon/marathon_screen.dart index a39b998..2d951b0 100644 --- a/lib/ui/marathon/marathon_screen.dart +++ b/lib/ui/marathon/marathon_screen.dart @@ -23,7 +23,7 @@ import 'package:mohem_flutter_app/ui/marathon/widgets/question_card.dart'; import 'package:mohem_flutter_app/ui/marathon/widgets/question_card_builder.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/dialogs/confirm_dialog.dart'; +import 'package:mohem_flutter_app/widgets/bottom_sheets/confirm_bottom_sheet.dart'; import 'package:provider/provider.dart'; class MarathonScreen extends StatelessWidget { @@ -299,16 +299,16 @@ class MarathonScreen extends StatelessWidget { provider.getMarathonDetailsFromApi(); Navigator.of(context).popUntil(ModalRoute.withName(AppRoutes.dashboard)); } else { - showDialog( - context: context, - builder: (BuildContext context) => ConfirmDialog( - message: LocaleKeys.youWantToLeaveMarathon.tr(), - onTap: () { - provider.resetValues(); - provider.getMarathonDetailsFromApi(); - Navigator.of(context).popUntil(ModalRoute.withName(AppRoutes.dashboard)); - }, - ), + ConfirmBottomSheet.show( + context, + message: LocaleKeys.youWantToLeaveMarathon.tr(), + confirmText: LocaleKeys.yes.tr(), + cancelText: LocaleKeys.no.tr(), + onConfirm: () { + provider.resetValues(); + provider.getMarathonDetailsFromApi(); + Navigator.of(context).popUntil(ModalRoute.withName(AppRoutes.dashboard)); + }, ); } }, @@ -318,15 +318,15 @@ class MarathonScreen extends StatelessWidget { provider.getMarathonDetailsFromApi(); Navigator.of(context).popUntil(ModalRoute.withName(AppRoutes.dashboard)); } else { - showDialog( - context: context, - builder: (BuildContext context) => ConfirmDialog( - message: LocaleKeys.youWantToLeaveMarathon.tr(), - onTap: () { - provider.resetValues(); - Navigator.of(context).popUntil(ModalRoute.withName(AppRoutes.dashboard)); - }, - ), + ConfirmBottomSheet.show( + context, + message: LocaleKeys.youWantToLeaveMarathon.tr(), + confirmText: LocaleKeys.yes.tr(), + cancelText: LocaleKeys.no.tr(), + onConfirm: () { + provider.resetValues(); + Navigator.of(context).popUntil(ModalRoute.withName(AppRoutes.dashboard)); + }, ); } }, @@ -390,15 +390,15 @@ class MarathonScreen extends StatelessWidget { ), ), onWillPop: () { - showDialog( - context: context, - builder: (BuildContext context) => ConfirmDialog( - message: LocaleKeys.youWantToLeaveMarathon.tr(), - onTap: () { - provider.resetValues(); - Navigator.of(context).popUntil(ModalRoute.withName(AppRoutes.dashboard)); - }, - ), + ConfirmBottomSheet.show( + context, + message: LocaleKeys.youWantToLeaveMarathon.tr(), + confirmText: LocaleKeys.yes.tr(), + cancelText: LocaleKeys.no.tr(), + onConfirm: () { + provider.resetValues(); + Navigator.of(context).popUntil(ModalRoute.withName(AppRoutes.dashboard)); + }, ); return Future.value(false); }, diff --git a/lib/ui/marathon/marathon_waiting_screen.dart b/lib/ui/marathon/marathon_waiting_screen.dart index 5dfb3ee..50d3852 100644 --- a/lib/ui/marathon/marathon_waiting_screen.dart +++ b/lib/ui/marathon/marathon_waiting_screen.dart @@ -10,7 +10,7 @@ import 'package:mohem_flutter_app/extensions/widget_extensions.dart'; import 'package:mohem_flutter_app/generated/locale_keys.g.dart'; import 'package:mohem_flutter_app/ui/marathon/marathon_provider.dart'; import 'package:mohem_flutter_app/widgets/app_bar_widget.dart'; -import 'package:mohem_flutter_app/widgets/dialogs/confirm_dialog.dart'; +import 'package:mohem_flutter_app/widgets/bottom_sheets/confirm_bottom_sheet.dart'; import 'package:provider/provider.dart'; class MarathonWaitingScreen extends StatelessWidget { @@ -21,15 +21,15 @@ class MarathonWaitingScreen extends StatelessWidget { MarathonProvider provider = context.watch(); return WillPopScope( onWillPop: () { - showDialog( - context: context, - builder: (BuildContext context) => ConfirmDialog( - message: LocaleKeys.youWantToLeaveMarathon.tr(), - onTap: () { - provider.resetValues(); - Navigator.of(context).popUntil(ModalRoute.withName(AppRoutes.dashboard)); - }, - ), + ConfirmBottomSheet.show( + context, + message: LocaleKeys.youWantToLeaveMarathon.tr(), + confirmText: LocaleKeys.yes.tr(), + cancelText: LocaleKeys.no.tr(), + onConfirm: () { + provider.resetValues(); + Navigator.of(context).popUntil(ModalRoute.withName(AppRoutes.dashboard)); + }, ); return Future.value(false); }, @@ -38,27 +38,27 @@ class MarathonWaitingScreen extends StatelessWidget { context, title: LocaleKeys.brainMarathon.tr(), onHomeTapped: () { - showDialog( - context: context, - builder: (BuildContext context) => ConfirmDialog( - message: LocaleKeys.youWantToLeaveMarathon.tr(), - onTap: () { - provider.resetValues(); - Navigator.of(context).popUntil(ModalRoute.withName(AppRoutes.dashboard)); - }, - ), + ConfirmBottomSheet.show( + context, + message: LocaleKeys.youWantToLeaveMarathon.tr(), + confirmText: LocaleKeys.yes.tr(), + cancelText: LocaleKeys.no.tr(), + onConfirm: () { + provider.resetValues(); + Navigator.of(context).popUntil(ModalRoute.withName(AppRoutes.dashboard)); + }, ); }, onBackTapped: () { - showDialog( - context: context, - builder: (BuildContext context) => ConfirmDialog( - message: LocaleKeys.youWantToLeaveMarathon.tr(), - onTap: () { - provider.resetValues(); - Navigator.of(context).popUntil(ModalRoute.withName(AppRoutes.dashboard)); - }, - ), + ConfirmBottomSheet.show( + context, + message: LocaleKeys.youWantToLeaveMarathon.tr(), + confirmText: LocaleKeys.yes.tr(), + cancelText: LocaleKeys.no.tr(), + onConfirm: () { + provider.resetValues(); + Navigator.of(context).popUntil(ModalRoute.withName(AppRoutes.dashboard)); + }, ); }, ), diff --git a/lib/ui/mazaya/mazaya_discover_screen.dart b/lib/ui/mazaya/mazaya_discover_screen.dart new file mode 100644 index 0000000..225a5f3 --- /dev/null +++ b/lib/ui/mazaya/mazaya_discover_screen.dart @@ -0,0 +1,1148 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:mohem_flutter_app/api/offers_and_discounts_api_client.dart'; +import 'package:mohem_flutter_app/app_state/app_state.dart'; +import 'package:mohem_flutter_app/classes/colors.dart'; +import 'package:mohem_flutter_app/classes/consts.dart'; +import 'package:mohem_flutter_app/classes/utils.dart'; +import 'package:mohem_flutter_app/config/routes.dart'; +import 'package:mohem_flutter_app/extensions/string_extensions.dart'; +import 'package:mohem_flutter_app/generated/locale_keys.g.dart'; +import 'package:mohem_flutter_app/models/offers_and_discounts/get_offers_list.dart'; +import 'package:mohem_flutter_app/models/offers_and_discounts/wala_plus_category_model.dart'; +import 'package:mohem_flutter_app/models/offers_and_discounts/wala_plus_city_model.dart'; +import 'package:mohem_flutter_app/models/offers_and_discounts/wala_plus_retailer_model.dart'; +import 'package:mohem_flutter_app/models/post_params_model.dart'; +import 'package:mohem_flutter_app/ui/mazaya/widgets/mazaya_bottom_nav.dart'; +import 'package:mohem_flutter_app/ui/mazaya/widgets/mazaya_city_selector.dart'; +import 'package:mohem_flutter_app/widgets/app_bar_widget.dart'; +import 'package:mohem_flutter_app/widgets/bottom_sheets/confirm_bottom_sheet.dart'; +import 'package:shimmer/shimmer.dart'; + +// A single, fixed-category brand-list section (Travel Essentials, Home & Kitchen, ...). Swapping +// which category a section shows is a one-line change - just the categoryId passed to the +// constructor where the section is declared - since the fetch, the title resolution, and the UI +// binding all read off this one object instead of each having their own copy of the id. +class _MazayaCategorySection { + final String categoryId; + bool isLoading = true; + List retailers = []; + + // Resolved from the retailer response's own embedded `categories` data - fallback source for + // the title when the id isn't found in the top-level categories list (see _sectionTitle). + String? resolvedName; + + _MazayaCategorySection(this.categoryId); +} + +// Mazaya Discover screen (Figma node 890:2050, "Mazaya Deals - Curated for you"). Reached from +// the Mazaya banner on the main dashboard. Most sections are wired to the WalaPlus APIs +// (categories + per-category retailers). "Exclusive offers" instead reuses the legacy Offers & +// Discounts (ITG) API/implementation (lib/ui/screens/offers_and_discounts) - see +// _fetchExclusiveOffers and WalaPlusRetailerModel.fromExclusiveOffer. +class MazayaDiscoverScreen extends StatefulWidget { + const MazayaDiscoverScreen({Key? key}) : super(key: key); + + @override + State createState() => _MazayaDiscoverScreenState(); +} + +class _MazayaDiscoverScreenState extends State { + final TextEditingController _searchController = TextEditingController(); + + bool _isLoadingCategories = true; + List _categories = []; + + bool _isLoadingExclusiveOffers = true; + List _exclusiveOffers = []; + + WalaPlusCityModel? selectedCity; + bool _isLoadingLatest = true; + List _latestRetailers = []; + + // Fixed-category sections - to point a section at a different category, change only the id + // passed here. Everything else (fetch, title, UI) reads off the _MazayaCategorySection object. + final _MazayaCategorySection _travelSection = _MazayaCategorySection('p03'); + final _MazayaCategorySection _homeKitchenSection = _MazayaCategorySection( + 'Bw', + ); + + @override + void initState() { + super.initState(); + selectedCity = AppState().selectedMazayaCity; + _fetchCategories(); + _fetchExclusiveOffers(); + _initCityAndFetchSections(); + } + + Future _initCityAndFetchSections() async { + if (selectedCity == null) { + String savedCityJson = await Utils.getStringFromPrefs("MAZAYA_SELECTED_CITY"); + if (savedCityJson.isNotEmpty && mounted) { + try { + WalaPlusCityModel city = WalaPlusCityModel.fromJson(jsonDecode(savedCityJson)); + if (!city.isAllCities) { + AppState().setSelectedMazayaCity = city; + selectedCity = city; + if (mounted) setState(() {}); + } + } catch (e) { + debugPrint("Error restoring saved city: $e"); + } + } + } + if (mounted) { + _fetchLatestOnMazaya(); + _fetchCategorySection(_travelSection); + _fetchCategorySection(_homeKitchenSection); + } + } + + @override + void dispose() { + _searchController.dispose(); + super.dispose(); + } + + // Only drives the benefits strip now - Travel Essentials/Home & Kitchen/Latest on Mazaya each + // fetch independently (see _fetchCategorySection/_fetchLatestOnMazaya). Still fetched in + // parallel with those though, since it also feeds each fixed-category section's title (the + // top-level categories list is the primary source there - see _sectionTitle). + Future _fetchCategories() async { + setState(() => _isLoadingCategories = true); + try { + List categories = + await OffersAndDiscountsApiClient().getWalaPlusCategories( + context: context, + ); + if (!mounted) return; + setState(() { + _categories = categories; + _isLoadingCategories = false; + }); + } catch (e) { + debugPrint("MazayaDiscoverScreen: error fetching categories: $e"); + if (mounted) setState(() => _isLoadingCategories = false); + } + } + + // Fetches one fixed-category section (Travel Essentials, Home & Kitchen, ...) by whichever + // categoryId is set on the given _MazayaCategorySection. Title data comes from the first + // matching entry in each returned retailer's own embedded `categories` list (each retailer + // carries its full category info straight from the API) - used as a fallback by _sectionTitle + // when the id isn't found in the top-level categories list. + Future _fetchCategorySection(_MazayaCategorySection section) async { + setState(() => section.isLoading = true); + try { + WalaPlusRetailersResponse? response = await OffersAndDiscountsApiClient() + .getWalaPlusRetailers( + categoryId: section.categoryId, + page: 1, + perPage: 10, + cityId: selectedCity?.cityId, + context: context, + ); + if (!mounted) return; + List retailers = + response?.retailers ?? []; + String? categoryName; + for (WalaPlusRetailerModel r in retailers) { + for (WalaPlusCategoryModel c + in r.categories ?? []) { + if (c.id == section.categoryId && (c.name ?? '').isNotEmpty) { + categoryName = c.name; + break; + } + } + if (categoryName != null) break; + } + setState(() { + section.retailers = retailers; + section.resolvedName = categoryName; + section.isLoading = false; + }); + } catch (e) { + debugPrint( + "MazayaDiscoverScreen: error fetching category section '${section.categoryId}': $e", + ); + if (mounted) setState(() => section.isLoading = false); + } + } + + // "Latest on Mazaya" = newest retailers across all categories (WP_Sort_By=newest, WP_CategoryID + // omitted entirely - not just empty) - same WP_Sort_By param already wired for the main + // retailers grid's Sort by filter. No category filter here on purpose: the Figma mock itself + // mixes categories in this section (a logistics brand next to a food brand), confirming it's a + // cross-category feed. + Future _fetchLatestOnMazaya() async { + setState(() => _isLoadingLatest = true); + try { + WalaPlusRetailersResponse? response = await OffersAndDiscountsApiClient() + .getWalaPlusRetailers( + includeCategoryId: false, + sortBy: 'newest', + page: 1, + perPage: 10, + cityId: selectedCity?.cityId, + context: context, + ); + if (!mounted) return; + setState(() { + _latestRetailers = response?.retailers ?? []; + _isLoadingLatest = false; + }); + } catch (e) { + debugPrint("MazayaDiscoverScreen: error fetching latest-on-Mazaya: $e"); + if (mounted) setState(() => _isLoadingLatest = false); + } + } + + // Resolved live rather than stored once, since the two sources load independently and either + // can arrive first: the top-level categories list (the primary/most reliable source - present + // even if the section's category currently has zero retailers) and that category's own data + // embedded in each retailer (kept as a fallback for when the id isn't in the top-level list). + // fallbackTitle only shows while neither source has resolved a name yet. + String _sectionTitle(_MazayaCategorySection section, String fallbackTitle) { + bool isAr = mounted ? AppState().isArabic(context) : false; + for (WalaPlusCategoryModel c in _categories) { + if (c.id == section.categoryId) { + if (isAr && c.countryNameAr != null && c.countryNameAr!.trim().isNotEmpty) { + return c.countryNameAr!.trim(); + } + if ((c.name ?? '').isNotEmpty) { + return c.name!; + } + } + } + if (section.resolvedName != null && section.resolvedName!.isNotEmpty) { + return section.resolvedName!; + } + return fallbackTitle; + } + + // Same id-first/resolvedName-fallback lookup as _sectionTitle, but returns the full category + // model so "See all" can hand RetailersListScreen the actual category to pre-select (it re-syncs + // this against its own category list once that loads, so an id match is all that's required). + WalaPlusCategoryModel _categoryForSection( + _MazayaCategorySection section, + String fallbackTitle, + ) { + for (WalaPlusCategoryModel c in _categories) { + if (c.id == section.categoryId) return c; + } + return WalaPlusCategoryModel( + id: section.categoryId, + name: _sectionTitle(section, fallbackTitle), + ); + } + + // "Exclusive offers" reuses the legacy Offers & Discounts (ITG) API/implementation + // (lib/ui/screens/offers_and_discounts) rather than a WalaPlus endpoint - categoryID 0 is the + // same "all categories" value that screen's own initial load uses. Each result is mapped onto + // WalaPlusRetailerModel (see fromExclusiveOffer) so it renders through the same card/grid/detail + // screen as every other Mazaya section, flagged via isExclusive. + Future _fetchExclusiveOffers() async { + setState(() => _isLoadingExclusiveOffers = true); + try { + bool isAr = AppState().isArabic(context); + List offers = await OffersAndDiscountsApiClient() + .getOffersList(0, 10); + if (!mounted) return; + setState(() { + _exclusiveOffers = + offers + .map( + (o) => + WalaPlusRetailerModel.fromExclusiveOffer(o, isAr: isAr), + ) + .toList(); + _isLoadingExclusiveOffers = false; + }); + } catch (e) { + debugPrint("MazayaDiscoverScreen: error fetching exclusive offers: $e"); + if (mounted) setState(() => _isLoadingExclusiveOffers = false); + } + } + + // AppState().dependentName sometimes comes back with stray " - " separators (e.g. + // "John - Smith") - collapse those into plain spaces for display. + String? get _dependentGreetingName { + String? raw = AppState().dependentName; + if (raw == null || raw.trim().isEmpty) return null; + String cleaned = + raw + .replaceAll(RegExp(r'\s*-\s*'), ' ') + .replaceAll(RegExp(r'\s+'), ' ') + .trim(); + return cleaned.isEmpty ? null : cleaned; + } + + void _openRetailer(WalaPlusRetailerModel item) { + Navigator.pushNamed( + context, + AppRoutes.retailerDetailsScreen, + arguments: item, + ); + } + + Future _openRetailersGrid({WalaPlusCategoryModel? category}) async { + await Navigator.pushNamed( + context, + AppRoutes.retailersListScreen, + // Carries the currently selected city along so RetailersListScreen starts on it instead + // of defaulting back to "All Cities" - the user already chose a city on this screen. + arguments: { + if (category != null) 'category': category, + if (selectedCity != null) 'city': selectedCity, + }, + ); + if (mounted && selectedCity?.cityId != AppState().selectedMazayaCity?.cityId) { + setState(() => selectedCity = AppState().selectedMazayaCity); + _fetchLatestOnMazaya(); + _fetchCategorySection(_travelSection); + _fetchCategorySection(_homeKitchenSection); + } + } + + // Exclusive offers reuse the legacy Offers & Discounts API (see fetchExclusiveOffers in both + // this screen and RetailersListScreen), so "See all" opens RetailersListScreen's exclusive + // mode instead of a WalaPlus category. + Future _openExclusiveOffersGrid() async { + await Navigator.pushNamed( + context, + AppRoutes.retailersListScreen, + arguments: { + 'exclusiveMode': true, + if (selectedCity != null) 'city': selectedCity, + }, + ); + if (mounted && selectedCity?.cityId != AppState().selectedMazayaCity?.cityId) { + setState(() => selectedCity = AppState().selectedMazayaCity); + _fetchLatestOnMazaya(); + _fetchCategorySection(_travelSection); + _fetchCategorySection(_homeKitchenSection); + } + } + + // Tapping the search field opens the same "all categories / newest" data as "Latest on + // Mazaya"'s See all, but with the category hero hidden and the search bar auto-focused with + // the keyboard open (see RetailersListScreen's isSearchMode) instead of landing on Travel. + Future _openSearchGrid() async { + await Navigator.pushNamed( + context, + AppRoutes.retailersListScreen, + arguments: { + 'category': WalaPlusCategoryModel( + id: 'all', + name: LocaleKeys.allCategories.tr(), + ), + 'searchMode': true, + if (selectedCity != null) 'city': selectedCity, + }, + ); + if (mounted && selectedCity?.cityId != AppState().selectedMazayaCity?.cityId) { + setState(() => selectedCity = AppState().selectedMazayaCity); + _fetchLatestOnMazaya(); + _fetchCategorySection(_travelSection); + _fetchCategorySection(_homeKitchenSection); + } + } + + Future _onRefresh() async { + await Future.wait(>[ + _fetchCategories(), + _fetchLatestOnMazaya(), + _fetchCategorySection(_travelSection), + _fetchCategorySection(_homeKitchenSection), + _fetchExclusiveOffers(), + ]); + } + + Future _clearDependentPrefs() async { + await Utils.removeStringFromPrefs("P_DEP_IS_LOGGED_IN"); + await Utils.removeStringFromPrefs("P_DEP_NATIONAL_ID"); + await Utils.removeStringFromPrefs("P_DEP_BIRTH_DATE"); + await Utils.removeStringFromPrefs("P_DEP_TOKEN_ID"); + await Utils.removeStringFromPrefs("P_DEP_LOGIN_TOKEN_ID"); + await Utils.removeStringFromPrefs("P_DEP_AUTHENTICATION_TOKEN_ID"); + await Utils.removeStringFromPrefs("P_DEP_MOBILE_NUMBER"); + await Utils.removeStringFromPrefs("P_DEPENDENT_NAME"); + await Utils.removeStringFromPrefs("P_DEP_LOGIN_TYPE"); + await Utils.removeStringFromPrefs("MAZAYA_SELECTED_CITY"); + } + + // Same app bar actions as the previous dependent-login landing page + // (DashboardScreenForDependent, now reached from here instead): language toggle + logout, + // shown only for dependent-login sessions. Tapping back when there's nothing to pop (this + // screen is now the dependent-login landing page, so that's the common case) logs out instead. + void _performLogout() { + ConfirmBottomSheet.show( + context, + message: LocaleKeys.areYouSureWantToLogOut.tr(), + confirmText: LocaleKeys.yes.tr(), + cancelText: LocaleKeys.no.tr(), + isDestructive: false, + onConfirm: () async { + if (ApiConsts.isDependetLogin) { + await _clearDependentPrefs(); + } + ApiConsts.selectedMemberType = MemberType.employee; + ApiConsts.ISDependetLogin = false; + AppState().setDependentName = null; + AppState().setDependentDob = null; + AppState().setDependentLoginType = null; + AppState().setWalaplusDepInfoModel = null; + AppState().setSelectedMazayaCity = null; + await Utils.removeStringFromPrefs("MAZAYA_SELECTED_CITY"); + AppState().setPostParamsInitConfig(); + if (mounted) { + Navigator.pushNamedAndRemoveUntil( + context, + AppRoutes.login, + (Route route) => false, + ); + } + }, + ); + } + + Future _changeLanguage(Locale newLocale) async { + if (context.locale == newLocale) return; + await context.setLocale(newLocale); + PostParamsModel? obj = AppState().postParamsObject; + if (obj != null) { + obj.languageID = (newLocale.languageCode == "ar" ? 1 : 2); + AppState().setPostParamsModel(obj); + } + if (mounted) { + setState(() {}); + await _onRefresh(); + } + } + + Widget _buildLanguageIconButton(bool isAr) { + return Tooltip( + message: isAr ? 'English' : 'العربية', + child: InkWell( + borderRadius: BorderRadius.circular(8), + onTap: () { + if (isAr) { + _changeLanguage(const Locale("en", "US")); + } else { + _changeLanguage(const Locale("ar", "SA")); + } + }, + child: const Padding( + padding: EdgeInsets.all(3), + child: Icon( + Icons.language_rounded, + size: 24, + color: Color(0xFF1E293B), + ), + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + bool isAr = AppState().isArabic(context); + return Scaffold( + backgroundColor: Colors.white, + appBar: AppBarWidget( + context, + title: LocaleKeys.mazayaDeals.tr(), + showHomeButton: true, + // Discover is the dependent-login landing page - no back button there (logout is still + // reachable via the explicit logout action below). Employees reaching Discover from the + // regular dashboard can still pop back normally. + showBackButton: false, + onBackTapped: () { + if (Navigator.canPop(context)) { + Navigator.pop(context); + } + }, + actions: + ApiConsts.isDependetLogin + ? [ + _buildLanguageIconButton(isAr), + const SizedBox(width: 4), + InkWell( + borderRadius: BorderRadius.circular(8), + onTap: _performLogout, + child: Padding( + padding: const EdgeInsets.all(6), + child: SvgPicture.asset( + 'assets/images/drawer/logout.svg', + height: 20, + width: 20, + ), + ), + ), + const SizedBox(width: 12), + ] + : null, + ), + bottomNavigationBar: const MazayaBottomNav( + currentTab: MazayaTab.discover, + ), + body: RefreshIndicator( + onRefresh: _onRefresh, + color: MyColors.textMixColor, + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics( + parent: BouncingScrollPhysics(), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (ApiConsts.isDependetLogin && _dependentGreetingName != null) + Padding( + padding: const EdgeInsets.fromLTRB(22, 16, 22, 0), + child: LocaleKeys.helloName + .tr(args: [_dependentGreetingName!]) + .toText20(isBold: true, color: const Color(0xFF202F31)), + ), + Padding( + padding: const EdgeInsets.fromLTRB(22, 16, 22, 0), + child: _buildSearchField(), + ), + const SizedBox(height: 18), + _buildCategoriesSection(), + const SizedBox(height: 24), + _buildExclusiveOffersSection(), + const SizedBox(height: 24), + _buildBrandSection( + title: LocaleKeys.latestOnMazaya.tr(), + subtitle: LocaleKeys.goodFoodGreatCompanyEvenBetterPerks.tr(), + isLoading: _isLoadingLatest, + retailers: _latestRetailers, + backgroundColor: Colors.white, + onSeeAll: + () => _openRetailersGrid( + category: WalaPlusCategoryModel( + id: 'all', + name: LocaleKeys.allCategories.tr(), + ), + ), + ), + _buildBrandSection( + title: _sectionTitle( + _travelSection, + LocaleKeys.travelEssentials.tr(), + ), + subtitle: LocaleKeys.fromEverydayCareToYourNextAdventure.tr(), + isLoading: _travelSection.isLoading, + retailers: _travelSection.retailers, + backgroundColor: const Color(0xFFEFF2EF), + onSeeAll: + () => _openRetailersGrid( + category: _categoryForSection( + _travelSection, + LocaleKeys.travelEssentials.tr(), + ), + ), + ), + _buildBrandSection( + title: _sectionTitle( + _homeKitchenSection, + LocaleKeys.homeAndKitchen.tr(), + ), + subtitle: LocaleKeys.curatedPicksForYourHomeAndKitchen.tr(), + isLoading: _homeKitchenSection.isLoading, + retailers: _homeKitchenSection.retailers, + backgroundColor: Colors.white, + onSeeAll: + () => _openRetailersGrid( + category: _categoryForSection( + _homeKitchenSection, + LocaleKeys.homeAndKitchen.tr(), + ), + ), + ), + const SizedBox(height: 12), + ], + ), + ), + ), + ); + } + + Widget _buildSearchField() { + return Container( + height: 47, + padding: const EdgeInsets.symmetric(horizontal: 14), + decoration: BoxDecoration( + color: Colors.white, + border: Border.all(color: const Color(0xFFE8EAE4)), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + SvgPicture.asset( + "assets/icons/mazaya_search.svg", + width: 18, + height: 18, + colorFilter: const ColorFilter.mode( + Color(0xFF78817F), + BlendMode.srcIn, + ), + ), + const SizedBox(width: 10), + Expanded( + child: TextField( + controller: _searchController, + decoration: InputDecoration( + border: InputBorder.none, + isDense: true, + hintText: LocaleKeys.findABrandOrSomething.tr(), + hintStyle: const TextStyle( + color: Color(0xFF78817F), + fontSize: 12, + ), + ), + style: const TextStyle(fontSize: 12, color: Color(0xFF202F32)), + onTap: _openSearchGrid, + readOnly: true, + ), + ), + ], + ), + ); + } + + Widget _buildCategoriesSection() { + bool isAr = AppState().isArabic(context); + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 22), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + LocaleKeys.exploreYourBenefits.tr().toText12( + color: const Color(0xFF78817F), + weight: FontWeight.w500, + ), + InkWell( + borderRadius: BorderRadius.circular(8), + onTap: + () => showMazayaCitySelectionBottomSheet( + context, + selectedCityId: selectedCity?.cityId, + onCitySelected: (city) { + WalaPlusCityModel? newCity = city.isAllCities ? null : city; + AppState().setSelectedMazayaCity = newCity; + if (newCity != null) { + Utils.saveStringFromPrefs( + "MAZAYA_SELECTED_CITY", + jsonEncode(newCity.toJson()), + ); + } else { + Utils.removeStringFromPrefs("MAZAYA_SELECTED_CITY"); + } + setState(() => selectedCity = newCity); + _fetchLatestOnMazaya(); + _fetchCategorySection(_travelSection); + _fetchCategorySection(_homeKitchenSection); + }, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + SvgPicture.asset( + "assets/icons/mazaya_map_pin.svg", + width: 12, + height: 12, + colorFilter: const ColorFilter.mode( + Color(0xFF147D6B), + BlendMode.srcIn, + ), + ), + const SizedBox(width: 3), + (selectedCity?.getDisplayName(isAr) ?? + LocaleKeys.allCitiesOption.tr()) + .toText12(color: const Color(0xFF147D6B)), + const SizedBox(width: 3), + SvgPicture.asset( + "assets/icons/mazaya_chevron_down.svg", + width: 12, + height: 12, + colorFilter: const ColorFilter.mode( + Color(0xFF147D6B), + BlendMode.srcIn, + ), + ), + ], + ), + ), + ], + ), + const SizedBox(height: 16), + SizedBox( + height: 112, + child: + _isLoadingCategories + ? Shimmer.fromColors( + baseColor: const Color(0xFFE2E8F0), + highlightColor: const Color(0xFFF8FAFC), + child: ListView.separated( + scrollDirection: Axis.horizontal, + physics: const NeverScrollableScrollPhysics(), + itemCount: 6, + separatorBuilder: + (BuildContext _, int __) => + const SizedBox(width: 16), + itemBuilder: + (BuildContext _, int __) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 72, + height: 72, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + ), + ), + const SizedBox(height: 8), + Container( + width: 60, + height: 10, + color: Colors.white, + ), + ], + ), + ), + ) + : ListView.separated( + scrollDirection: Axis.horizontal, + physics: const BouncingScrollPhysics(), + itemCount: _categories.length, + separatorBuilder: + (BuildContext _, int __) => const SizedBox(width: 16), + itemBuilder: (BuildContext _, int index) { + WalaPlusCategoryModel category = _categories[index]; + return InkWell( + borderRadius: BorderRadius.circular(12), + onTap: () => _openRetailersGrid(category: category), + child: SizedBox( + width: 72, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(12), + child: + (category.image != null && + category.image!.isNotEmpty) + ? Image.network( + category.image!, + width: 72, + height: 72, + fit: BoxFit.cover, + errorBuilder: + (_, __, ___) => Container( + width: 72, + height: 72, + color: const Color( + 0xFFF1F5F9, + ), + child: const Icon( + Icons.category_outlined, + color: Color(0xFF94A3B8), + ), + ), + ) + : Container( + width: 72, + height: 72, + color: const Color(0xFFF1F5F9), + child: const Icon( + Icons.category_outlined, + color: Color(0xFF94A3B8), + ), + ), + ), + const SizedBox(height: 8), + (category.name ?? '').toText10( + color: const Color(0xFF111111), + ), + ], + ), + ), + ); + }, + ), + ), + ], + ), + ); + } + + Widget _buildExclusiveOffersSection() { + bool isAr = AppState().isArabic(context); + return Container( + width: double.infinity, + color: const Color(0xFF143F36), + padding: const EdgeInsets.only(top: 23, bottom: 24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 22), + child: LocaleKeys.theMazayaEdit.tr().toText10( + color: const Color(0xFFDCC58F), + ), + ), + const SizedBox(height: 4), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 22), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + LocaleKeys.exclusivelyYours.tr().toText20( + color: Colors.white, + weight: FontWeight.w600, + ), + InkWell( + onTap: _openExclusiveOffersGrid, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + LocaleKeys.seeAll.tr().toText11( + color: const Color(0xFFF1EBDD), + weight: FontWeight.w500, + ), + const SizedBox(width: 4), + RotatedBox( + quarterTurns: isAr ? 2 : 0, + child: SvgPicture.asset( + "assets/icons/mazaya_arrow_right.svg", + width: 15, + height: 15, + colorFilter: const ColorFilter.mode( + Color(0xFFF1EBDD), + BlendMode.srcIn, + ), + ), + ), + ], + ), + ), + ], + ), + const SizedBox(height: 4), + LocaleKeys.aLittleExtraJustForMazayaMembers.tr().toText11( + color: const Color(0xFFC2D4CC), + weight: FontWeight.w400, + ), + ], + ), + ), + const SizedBox(height: 17), + SizedBox( + height: 206, + child: + _isLoadingExclusiveOffers + ? Shimmer.fromColors( + baseColor: const Color(0xFF1B4C41), + highlightColor: const Color(0xFF225B4E), + child: ListView.separated( + scrollDirection: Axis.horizontal, + physics: const NeverScrollableScrollPhysics(), + padding: const EdgeInsets.only(left: 22), + itemCount: 2, + separatorBuilder: + (BuildContext _, int __) => + const SizedBox(width: 12), + itemBuilder: + (BuildContext _, int __) => Container( + width: 260, + height: 206, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + ), + ), + ), + ) + : ListView.separated( + scrollDirection: Axis.horizontal, + physics: const BouncingScrollPhysics(), + padding: const EdgeInsets.only(left: 22), + itemCount: _exclusiveOffers.length, + separatorBuilder: + (BuildContext _, int __) => const SizedBox(width: 12), + itemBuilder: + (BuildContext _, int index) => + _buildExclusiveOfferCard(_exclusiveOffers[index]), + ), + ), + ], + ), + ); + } + + Widget _buildExclusiveOfferCard(WalaPlusRetailerModel offer) { + String? imageUrl = offer.displayImage; + String offerText = offer.offerTitle ?? offer.description ?? ''; + return InkWell( + borderRadius: BorderRadius.circular(16), + onTap: () => _openRetailer(offer), + child: Container( + width: 260, + height: 206, + decoration: BoxDecoration( + color: const Color(0xFFF1EBDD), + borderRadius: BorderRadius.circular(16), + ), + clipBehavior: Clip.antiAlias, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + height: 128, + width: double.infinity, + child: + (imageUrl != null && imageUrl.isNotEmpty) + ? Image.network( + imageUrl, + fit: BoxFit.cover, + errorBuilder: + (_, __, ___) => Container( + color: const Color(0xFFE4DCC8), + child: const Icon( + Icons.image_outlined, + color: Color(0xFF9C8F6E), + ), + ), + ) + : Container( + color: const Color(0xFFE4DCC8), + child: const Icon( + Icons.image_outlined, + color: Color(0xFF9C8F6E), + ), + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + (offer.name ?? '').toText13( + color: const Color(0xFF143F36), + weight: FontWeight.w600, + maxlines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 3), + offerText.toText12( + color: const Color(0xFF143F36), + weight: FontWeight.w400, + maxLine: 1, + ), + ], + ), + ), + SvgPicture.asset( + "assets/icons/mazaya_arrow_up_right.svg", + width: 20, + height: 20, + colorFilter: const ColorFilter.mode( + Color(0xFF143F36), + BlendMode.srcIn, + ), + ), + ], + ), + ), + ), + ], + ), + ), + ); + } + + Widget _buildBrandSection({ + required String title, + required String subtitle, + required bool isLoading, + required List retailers, + required Color backgroundColor, + required VoidCallback onSeeAll, + }) { + if (!isLoading && retailers.isEmpty) return const SizedBox.shrink(); + bool isAr = AppState().isArabic(context); + return Container( + width: double.infinity, + color: backgroundColor, + padding: const EdgeInsets.symmetric(vertical: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 22), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + title.toText20( + color: const Color(0xFF202F32), + weight: FontWeight.w600, + ), + InkWell( + onTap: onSeeAll, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + LocaleKeys.seeAll.tr().toText11( + color: const Color(0xFF147D6B), + weight: FontWeight.w500, + ), + const SizedBox(width: 4), + RotatedBox( + quarterTurns: isAr ? 2 : 0, + child: SvgPicture.asset( + "assets/icons/mazaya_arrow_right.svg", + width: 15, + height: 15, + colorFilter: const ColorFilter.mode( + Color(0xFF147D6B), + BlendMode.srcIn, + ), + ), + ), + ], + ), + ), + ], + ), + const SizedBox(height: 4), + subtitle.toText11( + color: const Color(0xFF78817F), + weight: FontWeight.w400, + ), + ], + ), + ), + const SizedBox(height: 16), + SizedBox( + height: 110, + child: + isLoading + ? Shimmer.fromColors( + baseColor: const Color(0xFFE2E8F0), + highlightColor: const Color(0xFFF8FAFC), + child: ListView.separated( + scrollDirection: Axis.horizontal, + physics: const NeverScrollableScrollPhysics(), + padding: const EdgeInsets.only(left: 22), + itemCount: 4, + separatorBuilder: + (BuildContext _, int __) => + const SizedBox(width: 20), + itemBuilder: + (BuildContext _, int __) => const Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + CircleAvatar( + radius: 36, + backgroundColor: Colors.white, + ), + ], + ), + ), + ) + : ListView.separated( + scrollDirection: Axis.horizontal, + physics: const BouncingScrollPhysics(), + padding: const EdgeInsets.only(left: 22), + itemCount: retailers.length, + separatorBuilder: + (BuildContext _, int __) => const SizedBox(width: 20), + itemBuilder: + (BuildContext _, int index) => + _buildBrandItem(retailers[index]), + ), + ), + ], + ), + ); + } + + Widget _buildBrandItem(WalaPlusRetailerModel item) { + String? logoUrl = + (item.logoThumb != null && item.logoThumb!.isNotEmpty) + ? item.logoThumb + : item.logo; + return InkWell( + onTap: () => _openRetailer(item), + borderRadius: BorderRadius.circular(36), + child: SizedBox( + width: 80, + child: Column( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(36), + child: + (logoUrl != null && logoUrl.isNotEmpty) + ? Image.network( + logoUrl, + width: 72, + height: 72, + fit: BoxFit.cover, + errorBuilder: + (_, __, ___) => Container( + width: 72, + height: 72, + color: const Color(0xFFF1F5F9), + child: const Icon( + Icons.storefront_outlined, + color: Color(0xFF94A3B8), + ), + ), + ) + : Container( + width: 72, + height: 72, + color: const Color(0xFFF1F5F9), + child: const Icon( + Icons.storefront_outlined, + color: Color(0xFF94A3B8), + ), + ), + ), + const SizedBox(height: 6), + (item.name ?? '').toText11( + color: const Color(0xFF202F32), + weight: FontWeight.w600, + maxLine: 1, + overflow: TextOverflow.ellipsis, + isCenter: true, + ), + ], + ), + ), + ); + } +} diff --git a/lib/ui/mazaya/retailer_details_screen.dart b/lib/ui/mazaya/retailer_details_screen.dart new file mode 100644 index 0000000..4c448ea --- /dev/null +++ b/lib/ui/mazaya/retailer_details_screen.dart @@ -0,0 +1,2082 @@ +import 'dart:developer'; +import 'dart:ui' as ui; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +// import 'package:flutter_html/flutter_html.dart'; +import 'package:flutter_inappwebview/flutter_inappwebview.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:mohem_flutter_app/api/api_client.dart'; +import 'package:mohem_flutter_app/api/offers_and_discounts_api_client.dart'; +import 'package:mohem_flutter_app/app_state/app_state.dart'; +import 'package:mohem_flutter_app/classes/colors.dart'; +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/exceptions/api_exception.dart'; +import 'package:mohem_flutter_app/extensions/int_extensions.dart'; +import 'package:mohem_flutter_app/extensions/string_extensions.dart'; +import 'package:mohem_flutter_app/extensions/widget_extensions.dart'; +import 'package:mohem_flutter_app/generated/locale_keys.g.dart'; +import 'package:mohem_flutter_app/models/generic_response_model.dart'; +import 'package:mohem_flutter_app/models/offers_and_discounts/wala_plus_retailer_model.dart'; +import 'package:mohem_flutter_app/widgets/app_bar_widget.dart'; +import 'package:mohem_flutter_app/widgets/app_in_app_webview_screen.dart'; +import 'package:mohem_flutter_app/widgets/bottom_sheets/confirm_bottom_sheet.dart'; +import 'package:mohem_flutter_app/widgets/button/default_button.dart'; +import 'package:mohem_flutter_app/widgets/info_chip_widget.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:shimmer/shimmer.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class RetailerDetailsScreen extends StatefulWidget { + final WalaPlusRetailerModel? retailer; + final String? retailerId; + final String? userReference; + + const RetailerDetailsScreen({ + Key? key, + this.retailer, + this.retailerId, + this.userReference, + }) : super(key: key); + + @override + State createState() => _RetailerDetailsScreenState(); +} + +class _RetailerDetailsScreenState extends State { + WalaPlusRetailerModel? _retailer; + bool _isLoading = true; + bool _hasDetailsData = false; + String? _effectiveRetailerId; + String? _effectiveUserReference; + bool _hasFetched = false; + bool _isRedeeming = false; + bool _isFavorite = false; + bool _isTogglingFavorite = false; + + @override + void initState() { + super.initState(); + _retailer = widget.retailer; + _effectiveRetailerId = widget.retailerId ?? widget.retailer?.id; + _effectiveUserReference = widget.userReference; + _isFavorite = widget.retailer?.isFavorite ?? false; + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + if (_effectiveRetailerId == null) { + Object? args = ModalRoute.of(context)?.settings.arguments; + if (args is WalaPlusRetailerModel) { + _retailer ??= args; + _effectiveRetailerId = args.id; + _isFavorite = args.isFavorite ?? _isFavorite; + } else if (args is String) { + _effectiveRetailerId = args; + } else if (args is Map) { + if (args['retailer'] is WalaPlusRetailerModel) { + _retailer ??= args['retailer']; + _effectiveRetailerId = _retailer?.id; + _isFavorite = _retailer?.isFavorite ?? _isFavorite; + } else if (args['retailerId'] != null) { + _effectiveRetailerId = args['retailerId']?.toString(); + } + if (args['userReference'] != null) { + _effectiveUserReference = args['userReference']?.toString(); + } + } + } + + if (_effectiveUserReference == null || _effectiveUserReference!.isEmpty) { + String resolved = OffersAndDiscountsApiClient().resolveUserReference(); + if (resolved.isNotEmpty) { + _effectiveUserReference = resolved; + } + } + + // Exclusive offers are mapped straight from the legacy Offers & Discounts API into + // WalaPlusRetailerModel for display - their id isn't a real WalaPlus retailer id, so + // getWalaPlusRetailerById would just fail. Render the already-passed-in data as-is instead. + if (!_hasFetched && _retailer?.isExclusive == true) { + _hasFetched = true; + _isLoading = false; + _hasDetailsData = true; + } else if (!_hasFetched && _effectiveRetailerId != null) { + _hasFetched = true; + _fetchRetailerDetails(); + } + } + + Future _fetchRetailerDetails() async { + if (_effectiveRetailerId == null) { + if (mounted) { + setState(() { + _isLoading = false; + }); + } + return; + } + setState(() { + _isLoading = true; + }); + + try { + debugPrint( + "WalaPlusRetailerDetailsScreen: fetching details for retailer ID: $_effectiveRetailerId", + ); + WalaPlusRetailerModel? result = await OffersAndDiscountsApiClient() + .getWalaPlusRetailerById( + retailerId: _effectiveRetailerId!, + context: context, + userReference: _effectiveUserReference, + ); + if (result != null && mounted) { + if ((result.image == null || + result.image!.isEmpty || + result.image == "null") && + _retailer?.image != null) { + result.image = _retailer!.image; + } + if ((result.imageThumb == null || + result.imageThumb!.isEmpty || + result.imageThumb == "null") && + _retailer?.imageThumb != null) { + result.imageThumb = _retailer!.imageThumb; + } + if ((result.logo == null || + result.logo!.isEmpty || + result.logo == "null") && + _retailer?.logo != null) { + result.logo = _retailer!.logo; + } + if ((result.logoThumb == null || + result.logoThumb!.isEmpty || + result.logoThumb == "null") && + _retailer?.logoThumb != null) { + result.logoThumb = _retailer!.logoThumb; + } + if ((result.name == null || result.name!.isEmpty) && + _retailer?.name != null) { + result.name = _retailer!.name; + } + if ((result.type == null || result.type!.isEmpty) && + _retailer?.type != null) { + result.type = _retailer!.type; + } + if ((result.locations == null || result.locations!.isEmpty) && + _retailer?.locations != null && + _retailer!.locations!.isNotEmpty) { + result.locations = _retailer!.locations; + } + if ((result.offers == null || result.offers!.isEmpty) && + _retailer?.offers != null && + _retailer!.offers!.isNotEmpty) { + result.offers = _retailer!.offers; + } + if ((result.validity == null || result.validity!.isEmpty) && + _retailer?.validity != null) { + result.validity = _retailer!.validity; + } + if ((result.offerTitle == null || result.offerTitle!.isEmpty) && + _retailer?.offerTitle != null) { + result.offerTitle = _retailer!.offerTitle; + } + if ((result.website == null || + result.website!.isEmpty || + result.website == "null") && + _retailer?.website != null) { + result.website = _retailer!.website; + } + if (result.isFavorite == null && _retailer?.isFavorite != null) { + result.isFavorite = _retailer!.isFavorite; + } + if (result.contact == null && _retailer?.contact != null) { + result.contact = _retailer!.contact; + } + if ((result.email == null || result.email!.isEmpty) && + _retailer?.email != null) { + result.email = _retailer!.email; + } + if ((result.phone == null || result.phone!.isEmpty) && + _retailer?.phone != null) { + result.phone = _retailer!.phone; + } + if ((result.categories == null || result.categories!.isEmpty) && + _retailer?.categories != null && + _retailer!.categories!.isNotEmpty) { + result.categories = _retailer!.categories; + } + + setState(() { + _retailer = result; + _hasDetailsData = true; + _isFavorite = result.isFavorite ?? _isFavorite; + _retailer?.isFavorite = _isFavorite; + }); + } + } catch (e) { + debugPrint("Error fetching retailer by ID: $e"); + } finally { + if (mounted) { + setState(() { + _isLoading = false; + }); + } + } + } + + Future _openMerchantWebsite() async { + String? web = _retailer?.resolvedWebsite; + if (web == null || web.trim().isEmpty || web == "null") { + Utils.showToast(LocaleKeys.merchantWebsiteNotAvailable.tr()); + return; + } + String url = web.trim(); + try { + Uri uri = Uri.parse(url); + if (!uri.hasScheme) { + uri = Uri.parse("https://$url"); + } + bool launched = await launchUrl( + uri, + mode: LaunchMode.externalApplication, + ); + if (!launched) { + await launchUrl(uri); + } + } catch (e) { + debugPrint( + "Error opening merchant website with external application: $e", + ); + try { + await Navigator.push( + context, + MaterialPageRoute( + builder: + (BuildContext context) => AppInAppWebViewScreen( + url: url.startsWith("http") ? url : "https://$url", + title: + _retailer?.name ?? LocaleKeys.visitMerchantWebsite.tr(), + ), + ), + ); + } catch (err) { + debugPrint("Error opening merchant website in webview: $err"); + Utils.showToast(url); + } + } + } + + Future _toggleFavorite() async { + if (_isTogglingFavorite) return; + String retailerId = _retailer?.id ?? _effectiveRetailerId ?? ""; + if (retailerId.isEmpty) { + return; + } + + setState(() { + _isTogglingFavorite = true; + }); + Utils.showLoading(context); + + try { + bool targetFavorite = !_isFavorite; + GenericResponseModel? response; + if (targetFavorite) { + response = await OffersAndDiscountsApiClient().addWalaPlusFavorite( + retailerId: retailerId, + context: context, + userReference: _effectiveUserReference, + ); + } else { + response = await OffersAndDiscountsApiClient().removeWalaPlusFavorite( + retailerId: retailerId, + context: context, + userReference: _effectiveUserReference, + ); + } + + Utils.hideLoading(context); + + if (response != null && response.errorMessage == null) { + if (mounted) { + setState(() { + _isFavorite = targetFavorite; + _retailer?.isFavorite = targetFavorite; + }); + } + bool isAr = + Directionality.of(context) == ui.TextDirection.rtl || + AppState().isArabic(context); + String? msg; + if (isAr) { + msg = + targetFavorite + ? LocaleKeys.favoriteAddedSuccess.tr() + : LocaleKeys.favoriteRemovedSuccess.tr(); + } else { + msg = response.wpFavoritesAddRemoveMessage; + if (msg == null || msg.trim().isEmpty) { + msg = + targetFavorite + ? LocaleKeys.favoriteAddedSuccess.tr() + : LocaleKeys.favoriteRemovedSuccess.tr(); + } + } + Utils.showToast(msg, isSuccess: true); + } else { + String error = + response?.errorEndUserMessage ?? + response?.errorMessage ?? + LocaleKeys.favoriteError.tr(); + Utils.showToast(error); + } + } catch (e) { + Utils.hideLoading(context); + debugPrint("Error toggling favorite: $e"); + String errorMsg = LocaleKeys.favoriteError.tr(); + if (e is APIException && + e.error?.errorMessage != null && + e.error!.errorMessage!.isNotEmpty) { + errorMsg = e.error!.errorMessage!; + } + Utils.showToast(errorMsg); + } finally { + if (mounted) { + setState(() { + _isTogglingFavorite = false; + }); + } else { + _isTogglingFavorite = false; + } + } + } + + Future _openInAppBrowser(String url, {String? offerType}) async { + String title = _retailer?.name ?? LocaleKeys.offerDetails.tr(); + // Offer types seen so far: "promo", "discount", "unique_promo", "promo_unique_qr" (and + // possibly others) - any type that mentions "discount" or "qr" needs the in-page scanner, + // regardless of exact spelling/casing, so check by substring rather than exact match. + String normalizedOfferType = offerType?.trim().toLowerCase() ?? ""; + bool shouldStartScanning = + normalizedOfferType.contains("discount") || + normalizedOfferType.contains("qr"); + log( + "shouldStartScanning: ${shouldStartScanning.toString()} for offerType: ${offerType.toString()}", + ); + + // Ask for camera permission app-side, before the WebView (and the page's own + // getUserMedia() call) even exists. By the time the page asks, the OS decision is + // already made, so onPermissionRequest can answer instantly - no mid-flight dialog + // racing the page's own timeout for its original getUserMedia() call. + // QR/discount-style offers need the camera to work at all, so if the user denies here, + // don't open the transaction URL - plain promo offers never reach this check. + if (shouldStartScanning) { + try { + PermissionStatus status = await Permission.camera.status; + log("permission status: ${status.toString()}"); + if (status.isPermanentlyDenied) { + _showCameraPermissionSheet( + LocaleKeys.cameraPermissionPermanentlyDenied.tr(), + ); + return; + } + + if (!status.isGranted) { + status = await Permission.camera.request(); + } + + if (status.isPermanentlyDenied) { + _showCameraPermissionSheet( + LocaleKeys.cameraPermissionPermanentlyDenied.tr(), + ); + return; + } + + if (!status.isGranted) { + _showCameraPermissionSheet( + LocaleKeys.cameraPermissionRequiredToScan.tr(), + ); + return; + } + } catch (e) { + debugPrint( + "Error requesting camera permission before opening transaction URL: $e", + ); + _showCameraPermissionSheet( + LocaleKeys.cameraPermissionRequiredToScan.tr(), + ); + return; + } + } + + await Navigator.push( + context, + MaterialPageRoute( + builder: + (BuildContext context) => AppInAppWebViewScreen( + url: url, + title: title, + onPageLoaded: + shouldStartScanning ? _clickStartScanningButton : null, + ), + ), + ); + } + + // showModalBottomSheet is dismissible by tapping outside by default (isDismissible: true), + // so no extra wiring is needed for that. "Open Settings" is the only action - there's nothing + // useful to retry from here, the user has to leave the app to fix this. + void _showCameraPermissionSheet(String message) { + ConfirmBottomSheet.show( + context, + message: message, + confirmText: LocaleKeys.openSettings.tr(), + showCancelButton: false, + icon: Icons.videocam_off_outlined, + isDestructive: false, + onConfirm: () { + openAppSettings(); + }, + ); + } + + // Vue's @click handlers are component methods, not something on window, so we find the + // actual button by its text and click it (lets Vue's own handler fire). + // Two buttons matter here: "Start Scanning" (the initial trigger) and "Try Again" (shown + // instead of "Start Scanning" if the camera permission flow takes a while - clicking it + // retries the same flow). Either may need clicking more than once if permission stays + // pending, so instead of a one-shot "clicked" flag we keep watching for as long as the + // safety-net timeout allows, with a short cooldown after each click so a single button + // occurrence can't get double-clicked from back-to-back mutation events before the page + // has a chance to react. + // This can be injected more than once (e.g. once on page load, again once the native camera + // permission result comes back) - state lives on `window`, not in the closure, so a repeat + // call never starts a second observer or spams clicks. + // Only called for offer types mentioning "discount" or "qr" - plain promo offers just show a + // promo code, no scanner. + void _clickStartScanningButton(InAppWebViewController controller) { + controller.evaluateJavascript( + source: r""" + (function() { + var cooldownMs = 800; + + function tryClick() { + var now = Date.now(); + if (now - (window.__mohemmScanLastClickAt || 0) < cooldownMs) return; + + var buttons = document.querySelectorAll('button'); + for (var i = 0; i < buttons.length; i++) { + var text = (buttons[i].textContent || '').replace(/\s+/g, ' ').trim(); + if (text.indexOf('Start Scanning') !== -1 || text.indexOf('Try Again') !== -1) { + buttons[i].click(); + window.__mohemmScanLastClickAt = now; + return; + } + } + } + + tryClick(); + + if (window.__mohemmScanObserverActive) return; + window.__mohemmScanObserverActive = true; + + var observer = new MutationObserver(function() { + tryClick(); + }); + observer.observe(document.body, { childList: true, subtree: true }); + + // Safety net only - stop watching after a while so we don't observe forever + // (e.g. permission denied for good, or user left the page). + setTimeout(function() { + observer.disconnect(); + window.__mohemmScanObserverActive = false; + }, 120000); + })(); + """, + ); + } + + void _onUseOffer() { + if (_isRedeeming) return; + + List offers = _retailer?.offers ?? []; + log("offers.length: ${offers.length}"); + + if (offers.length > 1) { + _showOffersBottomSheet(offers); + } else if (offers.length == 1) { + WalaPlusOfferModel singleOffer = offers.first; + String offerId = + (singleOffer.id != null && singleOffer.id!.isNotEmpty) + ? singleOffer.id! + : (_retailer?.id ?? _effectiveRetailerId ?? ""); + String offerType = + (singleOffer.type != null && singleOffer.type!.isNotEmpty) + ? singleOffer.type! + : (_retailer?.offerType ?? "promo"); + _redeemOffer(offerId: offerId, offerType: offerType); + } else { + String offerId = _retailer?.id ?? _effectiveRetailerId ?? ""; + if (offerId.isEmpty) { + Utils.showToast(LocaleKeys.offerIdNotAvailable.tr()); + return; + } + _redeemOffer( + offerId: offerId, + offerType: _retailer?.offerType ?? "promo", + ); + } + } + + Future _redeemOffer({ + required String offerId, + required String offerType, + }) async { + if (_isRedeeming) return; + if (offerId.isEmpty) { + Utils.showToast(LocaleKeys.offerIdNotAvailable.tr()); + return; + } + + setState(() { + _isRedeeming = true; + }); + Utils.showLoading(context); + + try { + Map? res = await OffersAndDiscountsApiClient() + .redeemWalaPlusOffer( + offerId: offerId, + offerType: offerType, + context: context, + ); + + Utils.hideLoading(context); + + String? transactionUrl; + if (res != null) { + transactionUrl = + res['Transaction_Url'] ?? + res['TransactionUrl'] ?? + res['transaction_url'] ?? + res['transactionUrl'] ?? + res['Url'] ?? + res['url']; + } + + log("transactionUrl: ${transactionUrl.toString()}"); + + if (transactionUrl != null && transactionUrl.trim().isNotEmpty) { + await _openInAppBrowser(transactionUrl.trim(), offerType: offerType); + } else { + String? error = + res?['Error']?.toString() ?? + res?['error']?.toString() ?? + res?['ErrorMessage']?.toString() ?? + res?['message']?.toString(); + if (error != null && error.isNotEmpty && error != "null") { + Utils.showToast(error); + } else { + Utils.showToast(LocaleKeys.unableToRedeemOffer.tr()); + } + } + } catch (e) { + Utils.hideLoading(context); + debugPrint("Error redeeming offer: $e"); + Utils.showToast(LocaleKeys.errorRedeemingOffer.tr()); + } finally { + if (mounted) { + setState(() { + _isRedeeming = false; + }); + } else { + _isRedeeming = false; + } + } + } + + void _showOffersBottomSheet(List offers) { + bool isAr = + Directionality.of(context) == ui.TextDirection.rtl || + AppState().isArabic(context); + + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (BuildContext sheetContext) { + return Container( + constraints: BoxConstraints( + maxHeight: MediaQuery.of(sheetContext).size.height * 0.78, + ), + decoration: const BoxDecoration( + color: Color(0xFFF8F9FA), + borderRadius: BorderRadius.vertical(top: Radius.circular(24)), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + 12.height, + Center( + child: Container( + width: 44, + height: 4.5, + decoration: BoxDecoration( + color: const Color(0xFFCBD5E1), + borderRadius: BorderRadius.circular(2.5), + ), + ), + ), + 12.height, + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LocaleKeys.availableOffers.tr().toText18( + isBold: true, + color: const Color(0xFF1E293B), + ), + 4.height, + LocaleKeys.selectOfferToRedeem.tr().toText12( + color: const Color(0xFF64748B), + ), + ], + ), + ), + IconButton( + icon: const Icon( + Icons.close_rounded, + color: Color(0xFF64748B), + size: 24, + ), + onPressed: () => Navigator.pop(sheetContext), + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + ), + ], + ), + ), + 14.height, + const Divider(height: 1, color: Color(0xFFF1F5F9)), + Flexible( + child: ListView.separated( + shrinkWrap: true, + physics: const BouncingScrollPhysics(), + padding: const EdgeInsets.fromLTRB(20, 16, 20, 24), + itemCount: offers.length, + separatorBuilder: (_, __) => 14.height, + itemBuilder: (BuildContext _, int index) { + WalaPlusOfferModel offer = offers[index]; + return _buildOfferBottomSheetCard( + sheetContext, + offer, + isAr, + ); + }, + ), + ), + ], + ), + ); + }, + ); + } + + Widget _buildOfferBottomSheetCard( + BuildContext sheetContext, + WalaPlusOfferModel offer, + bool isAr, + ) { + String title = offer.title ?? ""; + String description = offer.description ?? ""; + String type = + (offer.type != null && offer.type!.isNotEmpty) + ? offer.type! + : "discount"; + String? expiredAt = offer.expiredAt; + + bool hasDifferentDescription = + description.trim().isNotEmpty && description.trim() != title.trim(); + + return Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: const Color(0xff000000).withOpacity(.05), + blurRadius: 10, + offset: const Offset(0, 3), + ), + ], + ), + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), + decoration: BoxDecoration( + color: const Color(0xFFE6F7F0), + borderRadius: BorderRadius.circular(8), + ), + child: type.toUpperCase().toText10( + isBold: true, + color: const Color(0xFF00A651), + ), + ), + if (expiredAt != null && + expiredAt.trim().isNotEmpty && + expiredAt != "null") ...[ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon( + Icons.event_outlined, + size: 12, + color: Color(0xFF64748B), + ), + const SizedBox(width: 4), + expiredAt.toText10(color: const Color(0xFF64748B)), + ], + ), + ], + ], + ), + 10.height, + title.toText14( + isBold: true, + color: const Color(0xFF1E293B), + maxlines: 2, + ), + if (hasDifferentDescription) ...[ + 6.height, + description.toText12(color: const Color(0xFF64748B), maxLine: 3), + ], + DefaultButton( + LocaleKeys.useOffer.tr(), + () { + Navigator.pop(sheetContext); + String offerId = + (offer.id != null && offer.id!.isNotEmpty) + ? offer.id! + : (_retailer?.id ?? _effectiveRetailerId ?? ""); + String offerType = + (offer.type != null && offer.type!.isNotEmpty) + ? offer.type! + : (_retailer?.offerType ?? "promo"); + _redeemOffer(offerId: offerId, offerType: offerType); + }, + height: 42, + borderRadius: 10, + fontSize: 14, + ), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + bool isAr = + Directionality.of(context) == ui.TextDirection.rtl || + AppState().isArabic(context); + + return Scaffold( + backgroundColor: Colors.white, + appBar: AppBarWidget( + context, + title: LocaleKeys.offerDetails.tr(), + showHomeButton: false, + ), + body: + (_isLoading && !_hasDetailsData) + ? _buildDetailsShimmer() + : Column( + children: [ + if (_isLoading && _hasDetailsData) + const LinearProgressIndicator( + minHeight: 2.5, + backgroundColor: Colors.transparent, + valueColor: AlwaysStoppedAnimation( + Color(0xFF168A89), + ), + ), + Expanded( + child: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 1. Breadcrumbs (Category / Company Name) + _buildBreadcrumbs(isAr), + 12.height, + + // 2. Hero Card (Top Image with Favorite Heart + Bottom #143F36 Dark Green Container) + _buildHeroCard(isAr), + 14.height, + + // 3. Store Type & City Filter Chips + _buildChipsRow(isAr), + 20.height, + + // 4. Merchant Contact (Website, Phone, Email) + _buildMerchantContactSection(isAr), + + // 5. Locations Section (Preview Cards + View All) + _buildLocationsSection(isAr), + + // 6. Good to Know & Terms and Conditions Section (standard Mazaya only) + if (_retailer?.isExclusive != true) ...[ + _buildGoodToKnowSection(isAr), + 20.height, + ], + ], + ), + ), + ), + + // 7. Bottom Redeem Offer Action Bar - not shown for exclusive offers (mapped + // from the legacy Offers & Discounts API), since there's no real WalaPlus + // offer/retailer ID behind them for redeemWalaPlusOffer to act on. + if (_retailer?.isExclusive != true) + _buildBottomActionBar(isAr), + ], + ), + ); + } + + // Skeleton mirroring the real card layout (breadcrumbs, hero card, chips, contact, locations) + Widget _buildDetailsShimmer() { + Widget block({double? width, double height = 14, double radius = 4}) { + return Container( + width: width, + height: height, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(radius), + ), + ); + } + + return Shimmer.fromColors( + baseColor: const Color(0xFFE2E8F0), + highlightColor: const Color(0xFFF8FAFC), + child: SingleChildScrollView( + physics: const NeverScrollableScrollPhysics(), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Breadcrumbs placeholder + block(width: 180, height: 14), + 14.height, + // Hero card placeholder + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + ), + clipBehavior: Clip.antiAlias, + child: Column( + children: [ + block(width: double.infinity, height: 200, radius: 0), + Container( + width: double.infinity, + color: const Color(0xFFCBD5E1), + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + block(width: 160, height: 20), + 8.height, + block(width: 220, height: 16), + 10.height, + block(width: 120, height: 12), + ], + ), + ), + ], + ), + ), + 16.height, + // Chips row placeholder + Row( + children: [ + block(width: 80, height: 32, radius: 20), + const SizedBox(width: 8), + block(width: 80, height: 32, radius: 20), + const SizedBox(width: 8), + block(width: 100, height: 32, radius: 20), + ], + ), + 20.height, + // Merchant contact placeholder + block(width: 140, height: 18), + 12.height, + block(width: double.infinity, height: 120, radius: 16), + 20.height, + // Locations placeholder + block(width: 100, height: 18), + 12.height, + block(width: double.infinity, height: 68, radius: 16), + 10.height, + block(width: double.infinity, height: 68, radius: 16), + ], + ), + ), + ); + } + + Widget _buildBreadcrumbs(bool isAr) { + String? categoryName = _retailer?.getDisplayCategory( + isAr: isAr, + fallbackCategories: OffersAndDiscountsApiClient.cachedCategories, + ); + String retailerName = _retailer?.name ?? ''; + + return Row( + children: [ + if (categoryName != null && categoryName.isNotEmpty) ...[ + categoryName.toText13(color: const Color(0xFF0D9488), isBold: true), + " / ".toText13(color: const Color(0xFF94A3B8)), + ], + retailerName + .toText13(color: const Color(0xFF64748B), isBold: false) + .expanded, + ], + ); + } + + Widget _buildHeroCard(bool isAr) { + String? categoryName = _retailer?.getDisplayCategory( + isAr: isAr, + fallbackCategories: OffersAndDiscountsApiClient.cachedCategories, + ); + String displayCity = _retailer?.getDisplayCity(isAr: isAr) ?? ''; + String subtitleText = ''; + if (categoryName != null && + categoryName.isNotEmpty && + displayCity.isNotEmpty) { + subtitleText = '$categoryName · $displayCity'; + } else if (categoryName != null && categoryName.isNotEmpty) { + subtitleText = categoryName; + } else if (displayCity.isNotEmpty) { + subtitleText = displayCity; + } + + String offerTitle = + _retailer?.offerTitle ?? + (_retailer?.offers != null && _retailer!.offers!.isNotEmpty + ? (_retailer!.offers!.first.title ?? '') + : ''); + + return Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: const Color(0xff000000).withOpacity(.08), + blurRadius: 16, + offset: const Offset(0, 4), + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Top Image Banner with Favorite Heart Button + SizedBox( + height: 200, + width: double.infinity, + child: Stack( + fit: StackFit.expand, + children: [ + InkWell( + onTap: _openMerchantWebsite, + child: + (_retailer?.displayImage != null && + _retailer!.displayImage!.isNotEmpty) + ? Image.network( + _retailer!.displayImage!, + fit: BoxFit.cover, + errorBuilder: + (_, __, ___) => Container( + color: const Color(0xFFF3F4F6), + child: const Icon( + Icons.broken_image, + size: 48, + color: Color(0xFF9CA3AF), + ), + ), + ) + : Container( + color: const Color(0xFFF3F4F6), + child: const Icon( + Icons.image_outlined, + size: 48, + color: Color(0xFF9CA3AF), + ), + ), + ), + // Exclusive offers (mapped from the legacy Offers & Discounts API) have no + // WalaPlus retailer ID, so there's nothing for add/removeWalaPlusFavorite to + // act on - hide the button rather than wiring it to a call that can't work. + if (_retailer?.isExclusive != true) + PositionedDirectional( + top: 14, + end: 14, + child: Container( + width: 40, + height: 40, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.14), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: IconButton( + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + icon: Icon( + _isFavorite + ? Icons.favorite_rounded + : Icons.favorite_border_rounded, + size: 22, + color: + _isFavorite + ? Colors.black + : const Color(0xFF1E293B), + ), + tooltip: LocaleKeys.favorite.tr(), + onPressed: + _isTogglingFavorite ? null : _toggleFavorite, + ), + ), + ), + ], + ), + ), + + // Bottom #143F36 Dark Green Container + Container( + width: double.infinity, + color: const Color(0xFF143F36), + padding: const EdgeInsets.fromLTRB(16, 16, 16, 18), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + (_retailer?.name ?? "").toText22( + isBold: true, + color: Colors.white, + isCentered: false, + ), + if (offerTitle.trim().isNotEmpty) ...[ + 6.height, + offerTitle.trim().toText16( + isBold: false, + color: Colors.white, + isCentered: false, + maxlines: 2, + ), + ], + if (subtitleText.isNotEmpty) ...[ + 8.height, + subtitleText.toText12( + color: Colors.white.withOpacity(0.85), + isCenter: false, + ), + ], + ], + ), + ), + ], + ), + ), + ); + } + + Widget _buildChipsRow(bool isAr) { + List chips = []; + + // Exclusive offers (mapped from the legacy Offers & Discounts API) carry no real + // type/locations/offers data, so there's nothing to infer Online/In Store from - the + // "neither known -> assume In Store" fallback below would otherwise show a wrong chip. + bool isExclusive = _retailer?.isExclusive == true; + + List types = _retailer?.normalizedTypes ?? []; + bool hasOnline = + types.contains('online') || (_retailer?.isOnlineStore == true); + bool hasInstore = + types.contains('instore') || + (_retailer?.isInstore == true) || + (_retailer?.hasPhysicalLocation == true) || + (_retailer?.locations != null && _retailer!.locations!.isNotEmpty); + + String rType = (_retailer?.type ?? '').toLowerCase(); + String rOfferType = (_retailer?.offerType ?? '').toLowerCase(); + if (rType.contains('online') || rOfferType.contains('online')) + hasOnline = true; + if (rType.contains('store') || rOfferType.contains('store')) + hasInstore = true; + if (rType.isNotEmpty && !rType.contains('online')) hasInstore = true; + + if (_retailer?.offers != null) { + for (var offer in _retailer!.offers!) { + String oType = (offer.type ?? '').toLowerCase(); + if (oType.contains('online')) hasOnline = true; + if (oType.contains('store') || + (oType.isNotEmpty && !oType.contains('online'))) + hasInstore = true; + } + } + if (!isExclusive && !hasOnline && !hasInstore) { + hasInstore = true; + } + if (isExclusive) { + hasOnline = false; + hasInstore = false; + } + + // Online chip + if (hasOnline) { + chips.add( + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 7), + decoration: BoxDecoration( + color: const Color(0xFFE6F7F0), + borderRadius: BorderRadius.circular(20), + ), + child: LocaleKeys.online.tr().toText12( + color: const Color(0xFF00A651), + isBold: true, + ), + ), + ); + } + + // In Store chip + if (hasInstore) { + chips.add( + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 7), + decoration: BoxDecoration( + color: const Color(0xFFEFF6FF), + borderRadius: BorderRadius.circular(20), + ), + child: LocaleKeys.inStore.tr().toText12( + color: const Color(0xFF2563EB), + isBold: true, + ), + ), + ); + } + + // Cities chips + List distinctCities = []; + if (_retailer?.locations != null) { + Set seen = {}; + for (var loc in _retailer!.locations!) { + String c = loc.getLocalizedCity(isAr); + if (c.isEmpty) c = loc.city ?? ''; + if (c.trim().isNotEmpty && !seen.contains(c.trim())) { + seen.add(c.trim()); + distinctCities.add(c.trim()); + } + } + } + + if (distinctCities.length <= 2) { + for (String c in distinctCities) { + chips.add( + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 7), + decoration: BoxDecoration( + color: const Color(0xFFF1F5F9), + borderRadius: BorderRadius.circular(20), + ), + child: c.toText12(color: const Color(0xFF334155), isBold: false), + ), + ); + } + } else { + for (int i = 0; i < 2; i++) { + chips.add( + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 7), + decoration: BoxDecoration( + color: const Color(0xFFF1F5F9), + borderRadius: BorderRadius.circular(20), + ), + child: distinctCities[i].toText12( + color: const Color(0xFF334155), + isBold: false, + ), + ), + ); + } + chips.add( + InkWell( + onTap: + () => _showAllLocationsBottomSheet(_retailer!.locations!, isAr), + borderRadius: BorderRadius.circular(20), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 7), + decoration: BoxDecoration( + color: const Color(0xFFF1F5F9), + borderRadius: BorderRadius.circular(20), + ), + child: LocaleKeys.moreCitiesCount + .tr(args: ['${distinctCities.length - 2}']) + .toText12(color: const Color(0xFF334155), isBold: false), + ), + ), + ); + } + + if (chips.isEmpty) { + return const SizedBox.shrink(); + } + + return Wrap(spacing: 8, runSpacing: 8, children: chips); + } + + Widget _buildMerchantContactSection(bool isAr) { + String? website = _retailer?.resolvedWebsite; + String? phone = _retailer?.resolvedPhone; + String? email = _retailer?.resolvedEmail; + + bool hasWebsite = + website != null && website.trim().isNotEmpty && website != "null"; + bool hasPhone = phone != null && phone.trim().isNotEmpty && phone != "null"; + bool hasEmail = email != null && email.trim().isNotEmpty && email != "null"; + + if (!hasWebsite && !hasPhone && !hasEmail) { + return const SizedBox.shrink(); + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LocaleKeys.merchantContact.tr().toText18( + isBold: true, + color: const Color(0xFF1E293B), + ), + 12.height, + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: const Color(0xFFE2E8F0)), + boxShadow: [ + BoxShadow( + color: const Color(0xff000000).withOpacity(.03), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + children: [ + if (hasWebsite) ...[ + InkWell( + onTap: _openMerchantWebsite, + borderRadius: BorderRadius.vertical( + top: const Radius.circular(16), + bottom: + (!hasPhone && !hasEmail) + ? const Radius.circular(16) + : Radius.zero, + ), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 14, + ), + child: Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LocaleKeys.website.tr().toText12( + color: const Color(0xFF64748B), + ), + 4.height, + website.toText14( + color: const Color(0xFF2563EB), + isUnderLine: true, + weight: FontWeight.w500, + ), + ], + ).expanded, + const Icon( + Icons.open_in_new_rounded, + size: 20, + color: Color(0xFF64748B), + ), + ], + ), + ), + ), + ], + if (hasPhone) ...[ + if (hasWebsite) + const Divider( + height: 1, + thickness: 1, + color: Color(0xFFF1F5F9), + ), + InkWell( + onTap: () async { + try { + Uri phoneUri = Uri(scheme: 'tel', path: phone); + await launchUrl(phoneUri); + } catch (e) { + debugPrint("Error launching phone: $e"); + } + }, + borderRadius: + (!hasWebsite && !hasEmail) + ? BorderRadius.circular(16) + : (!hasEmail + ? const BorderRadius.vertical( + bottom: Radius.circular(16), + ) + : BorderRadius.zero), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 14, + ), + child: Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LocaleKeys.phone.tr().toText12( + color: const Color(0xFF64748B), + ), + 4.height, + phone.toText14( + color: const Color(0xFF1E293B), + weight: FontWeight.w600, + ), + ], + ).expanded, + const Icon( + Icons.phone_outlined, + size: 20, + color: Color(0xFF64748B), + ), + ], + ), + ), + ), + ], + if (hasEmail) ...[ + if (hasWebsite || hasPhone) + const Divider( + height: 1, + thickness: 1, + color: Color(0xFFF1F5F9), + ), + InkWell( + onTap: () async { + try { + Uri emailUri = Uri(scheme: 'mailto', path: email); + await launchUrl(emailUri); + } catch (e) { + debugPrint("Error launching email: $e"); + } + }, + borderRadius: BorderRadius.vertical( + top: + (!hasWebsite && !hasPhone) + ? const Radius.circular(16) + : Radius.zero, + bottom: const Radius.circular(16), + ), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 14, + ), + child: Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LocaleKeys.email.tr().toText12( + color: const Color(0xFF64748B), + ), + 4.height, + email.toText14( + color: const Color(0xFF1E293B), + weight: FontWeight.w600, + ), + ], + ).expanded, + const Icon( + Icons.mail_outline_rounded, + size: 20, + color: Color(0xFF64748B), + ), + ], + ), + ), + ), + ], + ], + ), + ), + 20.height, + ], + ); + } + + Widget _buildGoodToKnowSection(bool isAr) { + if (_retailer?.isExclusive == true) { + return const SizedBox.shrink(); + } + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + 10.height, + LocaleKeys.goodToKnow.tr().toText18( + isBold: true, + color: const Color(0xFF1E293B), + ), + 14.height, + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Padding( + padding: EdgeInsets.only(top: 2), + child: Icon( + Icons.info_outline_rounded, + color: Color(0xFF0D9488), + size: 20, + ), + ), + 10.width, + LocaleKeys.goodToKnowDesc + .tr() + .toText13(color: const Color(0xFF475569)) + .expanded, + ], + ), + 16.height, + const Divider(height: 1, thickness: 1, color: Color(0xFFE2E8F0)), + 14.height, + InkWell( + onTap:_openMerchantWebsite, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + LocaleKeys.termsAndConditions.tr().toText14( + isBold: true, + color: const Color(0xFF1E293B), + ), + const Icon( + Icons.open_in_new_rounded, + size: 20, + color: Color(0xFF64748B), + ), + ], + ), + ), + 14.height, + const Divider(height: 1, thickness: 1, color: Color(0xFFE2E8F0)), + 14.height, + // Row( + // mainAxisAlignment: MainAxisAlignment.spaceBetween, + // children: [ + // LocaleKeys.haveAQuestionAboutOffer.tr().toText13( + // color: const Color(0xFF64748B), + // ), + // InkWell( + // onTap: () { + // // Get help link will be provided later + // }, + // child: LocaleKeys.getHelp.tr().toText13( + // color: const Color(0xFF0D9488), + // isBold: true, + // ), + // ), + // ], + // ), + ], + ); + } + + Widget _buildBottomActionBar(bool isAr) { + return Container( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 24), + decoration: BoxDecoration( + color: Colors.white, + border: const Border( + top: BorderSide(color: Color(0xFFF1F5F9), width: 1), + ), + boxShadow: [ + BoxShadow( + color: const Color(0xff000000).withOpacity(.06), + blurRadius: 10, + offset: const Offset(0, -2), + ), + ], + ), + child: SizedBox( + width: double.infinity, + height: 52, + child: Material( + color: + _isRedeeming ? const Color(0xFF94A3B8) : const Color(0xFF169E8F), + borderRadius: BorderRadius.circular(14), + child: InkWell( + onTap: _isRedeeming ? null : _onUseOffer, + borderRadius: BorderRadius.circular(14), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: + _isRedeeming + ? const Center( + child: SizedBox( + width: 22, + height: 22, + child: CircularProgressIndicator( + strokeWidth: 2.5, + valueColor: AlwaysStoppedAnimation( + Colors.white, + ), + ), + ), + ) + : Row( + children: [ + LocaleKeys.redeemOffer.tr().toText16( + isBold: true, + color: Colors.white, + isCentered: false, + ), + const Spacer(), + Icon( + isAr + ? Icons.arrow_back_rounded + : Icons.arrow_forward_rounded, + color: Colors.white, + size: 20, + ), + ], + ), + ), + ), + ), + ), + ); + } + + Widget _buildLocationsSection(bool isAr) { + List locations = _retailer?.locations ?? []; + if (locations.isEmpty) { + return const SizedBox.shrink(); + } + + int totalCount = locations.length; + List previewLocations = + totalCount <= 2 ? locations : locations.take(2).toList(); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + LocaleKeys.locations.tr().toText18( + isBold: true, + color: const Color(0xFF1E293B), + ), + InkWell( + onTap: () => _showAllLocationsBottomSheet(locations, isAr), + borderRadius: BorderRadius.circular(6), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + LocaleKeys.viewAll.tr().toText14( + color: const Color(0xFF0D9488), + isBold: true, + ), + 4.width, + Icon( + isAr + ? Icons.arrow_back_rounded + : Icons.arrow_forward_rounded, + size: 16, + color: const Color(0xFF0D9488), + ), + ], + ), + ), + ), + ], + ), + 12.height, + 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); + }, + ), + 16.height, + ], + ); + } + + Widget _buildLocationCard(WalaPlusRetailerLocation loc, bool isAr) { + String distanceText = loc.getDisplayDistance() ?? ""; + String cityText = loc.getLocalizedCity(isAr); + if (cityText.isEmpty) { + cityText = loc.city ?? ""; + } + String addressText = loc.address ?? ""; + + String primaryTitle = addressText.isNotEmpty ? addressText : cityText; + String? secondaryText = + distanceText.isNotEmpty + ? distanceText + : (addressText.isNotEmpty ? cityText : null); + + return InkWell( + onTap: () => _openMapForLocation(loc), + borderRadius: BorderRadius.circular(16), + child: Container( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: const Color(0xFFE2E8F0)), + boxShadow: [ + BoxShadow( + color: const Color(0xff000000).withOpacity(.03), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + children: [ + Container( + width: 40, + height: 40, + decoration: const BoxDecoration( + color: Color(0xFFF1F5F9), + shape: BoxShape.circle, + ), + child: const Icon( + Icons.location_on_outlined, + size: 20, + color: Color(0xFF64748B), + ), + ), + 12.width, + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + primaryTitle.toText16( + isBold: true, + color: const Color(0xFF1E293B), + maxlines: 1, + ), + if (secondaryText != null && secondaryText.isNotEmpty) ...[ + 3.height, + secondaryText.toText12( + color: const Color(0xFF0D9488), + weight: FontWeight.w600, + ), + ], + ], + ).expanded, + 10.width, + const Icon( + Icons.open_in_new_rounded, + size: 20, + color: Color(0xFF64748B), + ), + ], + ), + ), + ); + } + + 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( + 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(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, + ); + }, + ), + ), + ], + ), + ); + }, + ); + }, + ); + } + + 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"); + } + } +} diff --git a/lib/ui/mazaya/retailers_list_screen.dart b/lib/ui/mazaya/retailers_list_screen.dart new file mode 100644 index 0000000..7cc266c --- /dev/null +++ b/lib/ui/mazaya/retailers_list_screen.dart @@ -0,0 +1,2662 @@ +import 'dart:async'; +import 'dart:convert'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:geolocator/geolocator.dart'; +import 'package:mohem_flutter_app/api/login_api_client.dart'; +import 'package:mohem_flutter_app/api/offers_and_discounts_api_client.dart'; +import 'package:mohem_flutter_app/app_state/app_state.dart'; +import 'package:mohem_flutter_app/classes/colors.dart'; +import 'package:mohem_flutter_app/classes/consts.dart'; +import 'package:mohem_flutter_app/classes/utils.dart'; +import 'package:mohem_flutter_app/config/routes.dart'; +import 'package:mohem_flutter_app/extensions/int_extensions.dart'; +import 'package:mohem_flutter_app/extensions/string_extensions.dart'; +import 'package:mohem_flutter_app/generated/locale_keys.g.dart'; +import 'package:mohem_flutter_app/models/generic_response_model.dart'; +import 'package:mohem_flutter_app/models/offers_and_discounts/get_offers_list.dart'; +import 'package:mohem_flutter_app/models/offers_and_discounts/wala_plus_category_model.dart'; +import 'package:mohem_flutter_app/models/offers_and_discounts/wala_plus_city_model.dart'; +import 'package:mohem_flutter_app/models/offers_and_discounts/wala_plus_retailer_model.dart'; +import 'package:mohem_flutter_app/models/post_params_model.dart'; +import 'package:mohem_flutter_app/ui/mazaya/widgets/mazaya_city_selector.dart'; +import 'package:mohem_flutter_app/ui/mazaya/widgets/wala_plus_retailer_grid.dart'; +import 'package:mohem_flutter_app/widgets/bottom_sheets/confirm_bottom_sheet.dart'; +import 'package:mohem_flutter_app/widgets/button/default_button.dart'; +import 'package:mohem_flutter_app/widgets/info_chip_widget.dart'; +import 'package:mohem_flutter_app/widgets/location/Location.dart' as location; +import 'package:permission_handler/permission_handler.dart'; +import 'package:shimmer/shimmer.dart'; + +class RetailersListScreen extends StatefulWidget { + const RetailersListScreen({Key? key}) : super(key: key); + + @override + State createState() => _RetailersListScreenState(); +} + +class _RetailersListScreenState extends State { + List retailersList = []; + List _filteredRetailersList = []; + List categoriesList = []; + WalaPlusCategoryModel? selectedCategory; + String currentCategoryId = ""; + String selectedTypeFilter = "all"; + WalaPlusCityModel? selectedCity; + bool _isInitRouteArgs = false; + + // Reached from Discover's "Exclusive offers" See all - reuses the legacy Offers & Discounts + // (ITG) API/implementation (lib/ui/screens/offers_and_discounts) instead of a WalaPlus + // endpoint, so fetchCategories/fetchOffers/fetchSubcategories all short-circuit to + // fetchExclusiveOffers below rather than touching any WalaPlus-specific state or UI. + bool isExclusiveMode = false; + List _exclusiveOffers = []; + + // Reached by tapping Discover's search field - same "all categories / newest" data as the + // "Latest on Mazaya" See all, but the category hero is hidden (there's no specific category + // context, it's a pure search entry point) and the search bar auto-focuses with the keyboard + // open instead. + bool isSearchMode = false; + final FocusNode _searchFocusNode = FocusNode(); + + // Subcategories of whichever main category is currently selected - fetched via + // getWalaPlusCategories(parentId: ...) whenever a main category card is tapped. Empty/cleared + // when "All Categories" (or nothing) is selected, since there's no specific parent to ask for. + List subcategoriesList = []; + bool isSubcategoriesLoading = false; + String? _selectedSubcategoryId; + + // Filters bottom sheet state - all fully wired to the API now. Availability drives + // WP_Offer_Type (via selectedTypeFilter), _filterSortBy drives WP_Sort_By ('nearest' + // requires WP_Geo_Location, see _ensureLocationReadyForNearestSort), _filterDistance drives + // WP_Radius (null when "any"), _filterMinDiscount drives WP_Min_Discount (null when "any"). + // Committed on "Apply filters"/"Reset"; persisted here so reopening the sheet shows the + // last-applied selection (availability's source of truth is selectedTypeFilter itself). + String _filterSortBy = 'newest'; + String _filterDistance = 'any'; + String _filterMinDiscount = 'any'; + bool isNearestOffers = true; + bool isLoading = true; + bool isLoadingMore = false; + bool isCategoriesLoading = true; + bool hasMore = true; + bool _isFetchingPage = false; + int currentPage = 1; + int totalPages = 1; + final int pageSize = 10; + String searchQuery = ""; + Timer? _debounceTimer; + final TextEditingController _searchController = TextEditingController(); + final ScrollController _scrollController = ScrollController(); + bool _hasRegisteredMobileLoginInfo = false; + + @override + void initState() { + super.initState(); + _scrollController.addListener(_scrollListener); + _loadDependentName(); + WidgetsBinding.instance.addPostFrameCallback((_) async { + if (mounted) { + await _saveDependentSessionToPrefs(); + await _registerMobileLoginInfo(); + await fetchCategories(); + } + }); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + if (!_isInitRouteArgs) { + _isInitRouteArgs = true; + Object? args = ModalRoute.of(context)?.settings.arguments; + if (args is Map && args['exclusiveMode'] == true) { + isExclusiveMode = true; + } else if (args is WalaPlusCategoryModel) { + selectedCategory = args; + currentCategoryId = args.id ?? ""; + } else if (args is Map && args['category'] is WalaPlusCategoryModel) { + selectedCategory = args['category'] as WalaPlusCategoryModel; + currentCategoryId = selectedCategory?.id ?? ""; + } + if (args is Map && args['searchMode'] == true) { + isSearchMode = true; + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) _searchFocusNode.requestFocus(); + }); + } + // Preserves whichever city was already selected on Discover - starts here instead of + // resetting back to "All Cities" until the user explicitly changes it on this page. + if (args is Map && args['city'] is WalaPlusCityModel) { + selectedCity = args['city'] as WalaPlusCityModel; + } else if (AppState().selectedMazayaCity != null) { + selectedCity = AppState().selectedMazayaCity; + } + } + } + + Future _registerMobileLoginInfo() async { + if (_hasRegisteredMobileLoginInfo) return; + if (!ApiConsts.isDependetLogin) return; + _hasRegisteredMobileLoginInfo = true; + + try { + int loginType = AppState().dependentLoginType ?? 1; + String nationalId = AppState().getUserName ?? ""; + String dob = AppState().dependentDob ?? ""; + String name = AppState().dependentName ?? ""; + String mobile = AppState().postParamsObject?.mobileNumber ?? ""; + String tokenID = + (AppState().postParamsObject?.tokenID != null && + AppState().postParamsObject!.tokenID!.isNotEmpty) + ? AppState().postParamsObject!.tokenID! + : (AppState().authenticationTokenID ?? ""); + if (tokenID.isEmpty) { + tokenID = await Utils.getStringFromPrefs("P_DEP_TOKEN_ID"); + } + int sessionId = AppState().postParamsObject?.pSessionId ?? 0; + String email = AppState().postParamsObject?.pEmailAddress ?? ""; + String deviceToken = + AppState().getIsHuawei + ? AppState().getHuaweiPushToken + : ((AppState().getDeviceToken != null && + AppState().getDeviceToken!.isNotEmpty && + AppState().getDeviceToken != "null") + ? AppState().getDeviceToken! + : ""); + if (deviceToken.isEmpty || deviceToken == "null") { + String savedToken = await Utils.getStringFromPrefs( + "P_DEP_DEVICE_TOKEN", + ); + if (savedToken.isNotEmpty && savedToken != "null") { + deviceToken = savedToken; + } + } + + await LoginApiClient().insertMobileLoginInfoDependent( + loginType: loginType, + nationalId: nationalId, + dob: dob, + dependentName: name, + mobileNumber: mobile, + tokenID: tokenID, + sessionId: sessionId, + email: email, + deviceToken: deviceToken, + context: context, + ); + } catch (e) { + if (kDebugMode) { + print("Error calling insertMobileLoginInfoDependent: $e"); + } + } + } + + void _loadDependentName() { + if (AppState().dependentName != null && + AppState().dependentName!.isNotEmpty && + mounted) { + setState(() {}); + } + } + + String _cleanDependentName(String name) { + return name + .replaceAll(RegExp(r'\s*-\s*'), ' ') + .replaceAll(RegExp(r'\s+'), ' ') + .trim(); + } + + @override + void dispose() { + _debounceTimer?.cancel(); + _scrollController.removeListener(_scrollListener); + _scrollController.dispose(); + _searchController.dispose(); + _searchFocusNode.dispose(); + super.dispose(); + } + + void _scrollListener() { + if (!_scrollController.hasClients) return; + double maxScroll = _scrollController.position.maxScrollExtent; + double currentScroll = _scrollController.position.pixels; + // Trigger only when user scrolls to within 35px of the very bottom + if (currentScroll >= maxScroll - 35) { + if (!isLoading && !isLoadingMore && !_isFetchingPage && hasMore) { + fetchNextPage(); + } + } + } + + // Reuses the legacy Offers & Discounts (ITG) API/implementation + // (lib/ui/screens/offers_and_discounts) rather than a WalaPlus endpoint - categoryID 0 is the + // same "all categories" value that screen's own initial load uses. There's no query param on + // getOffersList, so search filters the already-fetched list client-side instead of re-fetching. + // Each result is mapped onto WalaPlusRetailerModel (see fromExclusiveOffer) so it renders + // through the exact same grid/card used everywhere else, flagged via isExclusive. + Future fetchExclusiveOffers({ + String? query, + bool forceRefresh = false, + }) async { + if (query != null) searchQuery = query; + if (forceRefresh) _exclusiveOffers = []; + setState(() { + isLoading = true; + hasMore = false; + }); + try { + if (_exclusiveOffers.isEmpty) { + bool isAr = AppState().isArabic(context); + List offers = await OffersAndDiscountsApiClient() + .getOffersList(0, 100); + _exclusiveOffers = + offers + .map( + (o) => + WalaPlusRetailerModel.fromExclusiveOffer(o, isAr: isAr), + ) + .toList(); + } + retailersList = _exclusiveOffers; + String q = searchQuery.trim().toLowerCase(); + _filteredRetailersList = + q.isEmpty + ? _exclusiveOffers + : _exclusiveOffers + .where((o) => (o.name ?? '').toLowerCase().contains(q)) + .toList(); + } catch (e) { + debugPrint("RetailersListScreen: error fetching exclusive offers: $e"); + _filteredRetailersList = []; + } finally { + if (mounted) { + setState(() { + isLoading = false; + isCategoriesLoading = false; + }); + } + } + } + + Future fetchCategories() async { + if (isExclusiveMode) { + await fetchExclusiveOffers(); + return; + } + setState(() { + isCategoriesLoading = true; + }); + + try { + List list = await OffersAndDiscountsApiClient() + .getWalaPlusCategories(context: context); + if (list.isEmpty) { + await Future.delayed(const Duration(milliseconds: 600)); + list = await OffersAndDiscountsApiClient().getWalaPlusCategories( + context: context, + ); + } + + if (mounted) { + categoriesList = list; + + if (selectedCategory != null) { + for (WalaPlusCategoryModel cat in list) { + if (cat.id == selectedCategory!.id) { + selectedCategory = cat; + break; + } + } + } else { + WalaPlusCategoryModel? defaultCat; + for (WalaPlusCategoryModel cat in list) { + String n = (cat.name ?? "").toLowerCase(); + if (n.contains("travel") || + n.contains("entertain") || + n.contains("سفر") || + n.contains("ترفيه")) { + defaultCat = cat; + break; + } + } + defaultCat ??= list.firstWhere( + (c) => + c.id != "all" && + c.id != "0" && + (c.name ?? "").toLowerCase() != "all" && + c.name != "الكل" && + c.name != "الجميع", + orElse: + () => + list.isNotEmpty + ? list.first + : WalaPlusCategoryModel( + id: "1", + name: "Travel & Entertainment", + ), + ); + selectedCategory = defaultCat; + } + + currentCategoryId = selectedCategory?.id ?? ""; + setState(() {}); + + if (currentCategoryId.isNotEmpty) { + await fetchSubcategories(currentCategoryId); + await fetchOffers(categoryId: currentCategoryId); + } + } + } catch (e) { + debugPrint("Error fetching WalaPlus categories (attempt 1): $e"); + try { + await Future.delayed(const Duration(milliseconds: 600)); + List retryList = + await OffersAndDiscountsApiClient().getWalaPlusCategories( + context: context, + ); + if (mounted && retryList.isNotEmpty) { + categoriesList = retryList; + if (selectedCategory != null) { + for (WalaPlusCategoryModel cat in retryList) { + if (cat.id == selectedCategory!.id) { + selectedCategory = cat; + break; + } + } + } else { + WalaPlusCategoryModel? defaultCat; + for (WalaPlusCategoryModel cat in retryList) { + String n = (cat.name ?? "").toLowerCase(); + if (n.contains("travel") || + n.contains("entertain") || + n.contains("سفر") || + n.contains("ترفيه")) { + defaultCat = cat; + break; + } + } + defaultCat ??= retryList.firstWhere( + (c) => + c.id != "all" && + c.id != "0" && + (c.name ?? "").toLowerCase() != "all" && + c.name != "الكل" && + c.name != "الجميع", + orElse: () => retryList.first, + ); + selectedCategory = defaultCat; + } + currentCategoryId = selectedCategory?.id ?? ""; + setState(() {}); + if (currentCategoryId.isNotEmpty) { + await fetchSubcategories(currentCategoryId); + await fetchOffers(categoryId: currentCategoryId); + } + return; + } + } catch (retryEx) { + debugPrint("Error fetching WalaPlus categories (attempt 2): $retryEx"); + } + + if (mounted) { + if (selectedCategory == null) { + selectedCategory = WalaPlusCategoryModel( + id: "1", + name: "Travel & Entertainment", + ); + currentCategoryId = "1"; + } + setState(() {}); + await fetchOffers(categoryId: currentCategoryId); + } + } finally { + if (mounted) { + setState(() { + isCategoriesLoading = false; + }); + } + } + } + + // Fetches the subcategories under a main category (WP_ParentId). "All Categories" (or no + // category) has no specific parent, so that just clears the list instead of calling the API. + Future fetchSubcategories(String? parentId) async { + if (isExclusiveMode) return; + if (parentId == null || parentId.isEmpty || parentId == "all") { + setState(() { + subcategoriesList = []; + _selectedSubcategoryId = null; + }); + return; + } + + setState(() { + isSubcategoriesLoading = true; + }); + + try { + List list = await OffersAndDiscountsApiClient() + .getWalaPlusCategories(context: context, parentId: parentId); + if (mounted) { + setState(() { + subcategoriesList = list; + _selectedSubcategoryId = null; + }); + } + } catch (e) { + debugPrint("Error fetching WalaPlus subcategories: $e"); + if (mounted) { + setState(() { + subcategoriesList = []; + }); + } + } finally { + if (mounted) { + setState(() { + isSubcategoriesLoading = false; + }); + } + } + } + + String? _getUserReferenceForFavorites() { + String ref = OffersAndDiscountsApiClient().resolveUserReference(); + if (ref.trim().isNotEmpty) return ref.trim(); + String? userName = AppState().getUserName; + if (userName != null && userName.trim().isNotEmpty) return userName.trim(); + return null; + } + + Future fetchOffers({String? categoryId, String? query}) async { + if (isExclusiveMode) { + await fetchExclusiveOffers(query: query); + return; + } + String targetCategoryId = + categoryId ?? + _selectedSubcategoryId ?? + (currentCategoryId.isEmpty + ? (selectedCategory?.id ?? "") + : currentCategoryId); + if (query != null) { + searchQuery = query; + } + + bool wantsNearest = _filterSortBy == 'nearest'; + if (wantsNearest && !await _ensureLocationReadyForNearestSort()) { + return; + } + + setState(() { + isLoading = true; + currentPage = 1; + hasMore = true; + }); + + try { + String catParam = + (targetCategoryId == "all" || targetCategoryId == "0") + ? "" + : targetCategoryId; + String? offerTypeParam = OffersAndDiscountsApiClient.mapOfferTypeFilter( + selectedTypeFilter, + ); + String? queryParam = + searchQuery.trim().isNotEmpty ? searchQuery.trim() : null; + int? radiusParam = _radiusFromFilterDistance(_filterDistance); + int? minDiscountParam = + _filterMinDiscount == 'any' ? null : int.tryParse(_filterMinDiscount); + print( + "Fetching WalaPlus retailers: category='$catParam', offerType='$offerTypeParam', query='$queryParam', page=1, per_page=$pageSize", + ); + WalaPlusRetailersResponse? response = await OffersAndDiscountsApiClient() + .getWalaPlusRetailers( + categoryId: catParam, + page: 1, + perPage: pageSize, + offerType: offerTypeParam, + query: queryParam, + sortBy: _filterSortBy, + radius: radiusParam, + minDiscount: minDiscountParam, + cityId: + (selectedCity != null && !selectedCity!.isAllCities) + ? selectedCity!.cityId + : null, + context: wantsNearest ? context : null, + userReference: null, + ); + + retailersList = response?.retailers ?? []; + _filteredRetailersList = retailersList; + currentPage = 1; + totalPages = response?.pagesCount ?? 1; + hasMore = + currentPage < totalPages && + (response?.retailers?.isNotEmpty ?? false); + } catch (e) { + print("Error fetching WalaPlus retailers: $e"); + retailersList = []; + _filteredRetailersList = []; + } finally { + if (mounted) { + setState(() { + isLoading = false; + }); + } + } + } + + Future fetchNextPage() async { + if (isLoading || isLoadingMore || _isFetchingPage || !hasMore) return; + + bool wantsNearest = _filterSortBy == 'nearest'; + if (wantsNearest && !await _ensureLocationReadyForNearestSort()) { + return; + } + + _isFetchingPage = true; + setState(() { + isLoadingMore = true; + }); + + try { + int nextPage = currentPage + 1; + String effectiveCategoryId = _selectedSubcategoryId ?? currentCategoryId; + String catParam = + (effectiveCategoryId == "all" || effectiveCategoryId == "0") + ? "" + : effectiveCategoryId; + String? offerTypeParam = OffersAndDiscountsApiClient.mapOfferTypeFilter( + selectedTypeFilter, + ); + String? queryParam = + searchQuery.trim().isNotEmpty ? searchQuery.trim() : null; + int? radiusParam = _radiusFromFilterDistance(_filterDistance); + int? minDiscountParam = + (_filterMinDiscount == 'any') + ? null + : int.tryParse(_filterMinDiscount); + print( + "Fetching WalaPlus retailers next page: $nextPage of $totalPages, category: '$catParam', offerType: '$offerTypeParam', query: '$queryParam'", + ); + + WalaPlusRetailersResponse? response = await OffersAndDiscountsApiClient() + .getWalaPlusRetailers( + categoryId: catParam, + page: nextPage, + perPage: pageSize, + offerType: offerTypeParam, + query: queryParam, + sortBy: _filterSortBy, + radius: radiusParam, + minDiscount: minDiscountParam, + cityId: + (selectedCity != null && !selectedCity!.isAllCities) + ? selectedCity!.cityId + : null, + context: wantsNearest ? context : null, + userReference: null, + ); + + List newRetailers = response?.retailers ?? []; + if (newRetailers.isNotEmpty) { + currentPage = nextPage; + totalPages = response?.pagesCount ?? totalPages; + hasMore = currentPage < totalPages; + retailersList.addAll(newRetailers); + _filteredRetailersList = retailersList; + } else { + hasMore = false; + } + } catch (e) { + print("Error fetching next page of WalaPlus retailers: $e"); + } finally { + _isFetchingPage = false; + if (mounted) { + setState(() { + isLoadingMore = false; + }); + } + } + } + + Future _onRefresh() async { + if (isExclusiveMode) { + await fetchExclusiveOffers( + query: _searchController.text, + forceRefresh: true, + ); + return; + } + String effectiveCategoryId = + _selectedSubcategoryId ?? + (currentCategoryId.isEmpty + ? (selectedCategory?.id ?? "") + : currentCategoryId); + await Future.wait(>[ + fetchCategories(), + fetchOffers( + categoryId: effectiveCategoryId, + query: _searchController.text, + ), + ]); + } + + // '5km'/'10km'/'25km' -> 5/10/25 for WP_Radius; 'any' -> null (omit the param - no distance cap). + int? _radiusFromFilterDistance(String distance) { + if (distance == 'any') return null; + return int.tryParse(distance.replaceAll('km', '')); + } + + // WP_Sort_By=nearest requires WP_Geo_Location to be passed alongside it, so before fetching + // with "Nearest" selected we make sure location services + permission are actually available. + // Mirrors the existing attendance-flow location-permission pattern (see + // mark_attendance_widget.dart's checkHuaweiLocationPermission): services-off -> prompt to open + // settings, permission-not-granted -> request it, still denied -> toast. Never auto-retries the + // fetch - the user must grant access and trigger the fetch again themselves. + Future _ensureLocationReadyForNearestSort() async { + Completer enabledCompleter = Completer(); + location.Location.isEnabled((bool isEnabled) { + if (!enabledCompleter.isCompleted) enabledCompleter.complete(isEnabled); + }); + bool isEnabled = await enabledCompleter.future; + if (!isEnabled) { + if (mounted) { + ConfirmBottomSheet.show( + context, + message: LocaleKeys.locationServicesRequiredForNearestOffers.tr(), + confirmText: LocaleKeys.openSettings.tr(), + showCancelButton: false, + onConfirm: () async { + await Geolocator.openLocationSettings(); + }, + ); + } + return false; + } + + Completer permissionCompleter = Completer(); + location.Location.havePermission((bool hasPermission) { + if (!permissionCompleter.isCompleted) + permissionCompleter.complete(hasPermission); + }); + bool hasPermission = await permissionCompleter.future; + if (!hasPermission) { + bool granted = await _requestLocationPermission(); + if (!granted) { + Utils.showToast( + LocaleKeys.locationPermissionRequiredForNearestOffers.tr(), + ); + return false; + } + } + return true; + } + + Future _requestLocationPermission() async { + Map result = + await [Permission.location].request(); + return result[Permission.location] == PermissionStatus.granted || + result[Permission.locationAlways] == PermissionStatus.granted; + } + + void _onSearchChanged(String query) { + _debounceTimer?.cancel(); + searchQuery = query; + String effectiveCategoryId = _selectedSubcategoryId ?? currentCategoryId; + if (query.trim().isEmpty) { + fetchOffers(categoryId: effectiveCategoryId, query: ""); + } else { + _debounceTimer = Timer(const Duration(milliseconds: 600), () { + fetchOffers(categoryId: effectiveCategoryId, query: query.trim()); + }); + } + } + + Future _toggleFavorite(WalaPlusRetailerModel item) async { + String retailerId = item.id ?? ""; + if (retailerId.isEmpty) return; + + bool currentFav = item.isFavorite ?? false; + bool targetFav = !currentFav; + + setState(() { + item.isFavorite = targetFav; + }); + Utils.showLoading(context); + + try { + GenericResponseModel? response; + String? userRef = _getUserReferenceForFavorites(); + if (targetFav) { + response = await OffersAndDiscountsApiClient().addWalaPlusFavorite( + retailerId: retailerId, + context: context, + userReference: userRef, + ); + } else { + response = await OffersAndDiscountsApiClient().removeWalaPlusFavorite( + retailerId: retailerId, + context: context, + userReference: userRef, + ); + } + + Utils.hideLoading(context); + + if (response != null && response.errorMessage == null) { + bool isAr = AppState().isArabic(context); + String msg; + if (isAr) { + msg = + targetFav + ? LocaleKeys.favoriteAddedSuccess.tr() + : LocaleKeys.favoriteRemovedSuccess.tr(); + } else { + msg = response.wpFavoritesAddRemoveMessage ?? ""; + if (msg.trim().isEmpty) { + msg = + targetFav + ? LocaleKeys.favoriteAddedSuccess.tr() + : LocaleKeys.favoriteRemovedSuccess.tr(); + } + } + Utils.showToast(msg, isSuccess: true); + } else { + if (mounted) { + setState(() { + item.isFavorite = currentFav; + }); + } + String error = + response?.errorEndUserMessage ?? + response?.errorMessage ?? + LocaleKeys.favoriteError.tr(); + Utils.showToast(error); + } + } catch (e) { + Utils.hideLoading(context); + debugPrint("Error toggling favorite: $e"); + if (mounted) { + setState(() { + item.isFavorite = currentFav; + }); + } + Utils.showToast(LocaleKeys.favoriteError.tr()); + } + } + + void _openRetailer(WalaPlusRetailerModel item) async { + if (item.categories == null || item.categories!.isEmpty) { + if (selectedCategory != null) { + item.categories = [selectedCategory!]; + } else if (currentCategoryId.isNotEmpty && currentCategoryId != "all") { + for (var c in categoriesList) { + if (c.id?.toString() == currentCategoryId) { + item.categories = [c]; + break; + } + } + } + } + await Navigator.pushNamed( + context, + AppRoutes.retailerDetailsScreen, + arguments: item, + ); + if (mounted) { + setState(() {}); + } + } + + Future _saveDependentSessionToPrefs() async { + if (!ApiConsts.isDependetLogin) return; + try { + await Utils.saveStringFromPrefs("P_DEP_IS_LOGGED_IN", "true"); + String nationalId = AppState().getUserName ?? ""; + if (nationalId.isNotEmpty) { + await Utils.saveStringFromPrefs("P_DEP_NATIONAL_ID", nationalId); + } + String dob = AppState().dependentDob ?? ""; + if (dob.isNotEmpty) { + await Utils.saveStringFromPrefs("P_DEP_BIRTH_DATE", dob); + } + String name = AppState().dependentName ?? ""; + if (name.isNotEmpty) { + await Utils.saveStringFromPrefs("P_DEPENDENT_NAME", name); + } + String mobile = AppState().postParamsObject?.mobileNumber ?? ""; + if (mobile.isNotEmpty) { + await Utils.saveStringFromPrefs("P_DEP_MOBILE_NUMBER", mobile); + } + String tokenID = + (AppState().postParamsObject?.tokenID != null && + AppState().postParamsObject!.tokenID!.isNotEmpty) + ? AppState().postParamsObject!.tokenID! + : (AppState().authenticationTokenID ?? ""); + if (tokenID.isNotEmpty) { + await Utils.saveStringFromPrefs("P_DEP_TOKEN_ID", tokenID); + } + String logInTokenID = AppState().postParamsObject?.logInTokenID ?? ""; + if (logInTokenID.isNotEmpty) { + await Utils.saveStringFromPrefs("P_DEP_LOGIN_TOKEN_ID", logInTokenID); + } + String authTokenID = AppState().authenticationTokenID ?? ""; + if (authTokenID.isNotEmpty) { + await Utils.saveStringFromPrefs( + "P_DEP_AUTHENTICATION_TOKEN_ID", + authTokenID, + ); + } + int loginType = AppState().dependentLoginType ?? 1; + await Utils.saveStringFromPrefs("P_DEP_LOGIN_TYPE", loginType.toString()); + String deviceToken = + AppState().getIsHuawei + ? AppState().getHuaweiPushToken + : (AppState().getDeviceToken ?? ""); + if (deviceToken.isNotEmpty) { + await Utils.saveStringFromPrefs("P_DEP_DEVICE_TOKEN", deviceToken); + } + } catch (e) { + if (kDebugMode) print("Error saving dependent session to prefs: $e"); + } + } + + Future _clearDependentPrefs() async { + await Utils.removeStringFromPrefs("P_DEP_IS_LOGGED_IN"); + await Utils.removeStringFromPrefs("P_DEP_NATIONAL_ID"); + await Utils.removeStringFromPrefs("P_DEP_BIRTH_DATE"); + await Utils.removeStringFromPrefs("P_DEP_TOKEN_ID"); + await Utils.removeStringFromPrefs("P_DEP_LOGIN_TOKEN_ID"); + await Utils.removeStringFromPrefs("P_DEP_AUTHENTICATION_TOKEN_ID"); + await Utils.removeStringFromPrefs("P_DEP_MOBILE_NUMBER"); + await Utils.removeStringFromPrefs("P_DEPENDENT_NAME"); + await Utils.removeStringFromPrefs("P_DEP_LOGIN_TYPE"); + } + + void _performLogout() { + ConfirmBottomSheet.show( + context, + message: LocaleKeys.areYouSureWantToLogOut.tr(), + confirmText: LocaleKeys.yes.tr(), + cancelText: LocaleKeys.no.tr(), + isDestructive: false, + onConfirm: () async { + if (ApiConsts.isDependetLogin) { + await _clearDependentPrefs(); + } + ApiConsts.selectedMemberType = MemberType.employee; + ApiConsts.ISDependetLogin = false; + AppState().setDependentName = null; + AppState().setDependentDob = null; + AppState().setDependentLoginType = null; + AppState().setWalaplusDepInfoModel = null; + AppState().setPostParamsInitConfig(); + if (mounted) { + Navigator.pushNamedAndRemoveUntil( + context, + AppRoutes.login, + (Route route) => false, + ); + } + }, + ); + } + + Future _changeLanguage(Locale newLocale) async { + if (context.locale == newLocale) return; + await context.setLocale(newLocale); + PostParamsModel? obj = AppState().postParamsObject; + if (obj != null) { + obj.languageID = (newLocale.languageCode == "ar" ? 1 : 2); + AppState().setPostParamsModel(obj); + } + if (mounted) { + setState(() {}); + await fetchCategories(); + await fetchOffers( + categoryId: currentCategoryId.isEmpty ? "all" : currentCategoryId, + ); + } + } + + Widget _buildLanguageIconButton(bool isAr) { + return Tooltip( + message: isAr ? 'English' : 'العربية', + child: InkWell( + borderRadius: BorderRadius.circular(8), + onTap: () { + if (isAr) { + _changeLanguage(const Locale("en", "US")); + } else { + _changeLanguage(const Locale("ar", "SA")); + } + }, + child: const Padding( + padding: EdgeInsets.all(3), + child: Icon( + Icons.language_rounded, + size: 24, + color: Color(0xFF1E293B), + ), + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + bool isAr = AppState().isArabic(context); + return Scaffold( + backgroundColor: Colors.white, + appBar: AppBar( + backgroundColor: Colors.white, + elevation: 0, + surfaceTintColor: Colors.transparent, + automaticallyImplyLeading: false, + titleSpacing: 0, + title: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + children: [ + InkWell( + borderRadius: BorderRadius.circular(10), + onTap: () { + if (Navigator.canPop(context)) { + Navigator.pop(context); + } else if (ApiConsts.isDependetLogin) { + _performLogout(); + } + }, + child: const Padding( + padding: EdgeInsets.symmetric(horizontal: 4, vertical: 8), + child: Icon( + Icons.arrow_back_ios_new_rounded, + size: 20, + color: Color(0xFF1E293B), + ), + ), + ), + const SizedBox(width: 8), + LocaleKeys.mazayaDeals.tr().toText22( + isBold: true, + color: const Color(0xFF202F31), + ), + ], + ), + ), + actions: [ + if (ApiConsts.isDependetLogin) ...[ + _buildLanguageIconButton(isAr), + const SizedBox(width: 4), + InkWell( + borderRadius: BorderRadius.circular(8), + onTap: _performLogout, + child: Padding( + padding: const EdgeInsets.all(6), + child: SvgPicture.asset( + 'assets/images/drawer/logout.svg', + height: 20, + width: 20, + ), + ), + ), + const SizedBox(width: 12), + ], + ], + ), + body: SafeArea( + child: RefreshIndicator( + onRefresh: _onRefresh, + color: MyColors.textMixColor, + child: SingleChildScrollView( + controller: _scrollController, + physics: const AlwaysScrollableScrollPhysics( + parent: BouncingScrollPhysics(), + ), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 1. Sub-header: "Explore your benefits" | "📍 Riyadh" + _buildSubHeaderRow(isAr), + + // 2. Category Hero Section: Title, Subtitle, Category image - hidden when + // reached from Discover's search field, since there's no specific category + // context there and the search bar should be the first thing on screen. + if (!isSearchMode) _buildCategoryHero(isAr), + + // 3. Search Bar: "Find an escape or an experience" + _buildSearchBar(isAr), + + // 4. Filters & Subcategories Row - the filters (sort/distance/discount) and + // subcategories are WalaPlus-specific concepts that don't apply to exclusive + // offers (legacy Offers & Discounts API), so this row is skipped in that mode. + if (!isExclusiveMode) _buildSubcategoriesAndFilterRow(isAr), + + const SizedBox(height: 16), + + // 5. Offers Grid + WalaPlusRetailerGrid( + isLoading: isLoading, + retailers: _filteredRetailersList, + isAr: isAr, + isLoadingMore: isLoadingMore, + onTapRetailer: _openRetailer, + onFavoriteTap: _toggleFavorite, + mainAxisExtent: isExclusiveMode ? 190 : null, + ), + ], + ), + ), + ), + ), + ); + } + + Widget _buildSubHeaderRow(bool isAr) { + return Padding( + padding: const EdgeInsets.only(top: 4, bottom: 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + LocaleKeys.exploreYourBenefits.tr().toText13( + color: const Color(0xFF79807F), + ), + InkWell( + borderRadius: BorderRadius.circular(8), + onTap: _showCitySelectionBottomSheet, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon( + Icons.location_on_outlined, + size: 16, + color: Color(0xFF157D6B), + ), + const SizedBox(width: 4), + (selectedCity?.getDisplayName(isAr) ?? + LocaleKeys.allCitiesOption.tr()) + .toText13(isBold: true, color: const Color(0xFF157D6B)), + ], + ), + ), + ), + ], + ), + ); + } + + Widget _buildCategoryHero(bool isAr) { + bool isLatest = selectedCategory?.id == 'all'; + String categoryName = + isExclusiveMode + ? LocaleKeys.exclusivelyYours.tr() + : isLatest + ? LocaleKeys.latestOnMazaya.tr() + : selectedCategory?.name ?? + (isAr ? "السفر والترفيه" : "Travel & Entertainment"); + String subtitle = + isExclusiveMode + ? LocaleKeys.aLittleExtraJustForMazayaMembers.tr() + : isLatest + ? LocaleKeys.goodFoodGreatCompanyEvenBetterPerks.tr() + : (isAr + ? "مغامرتك القادمة، مع القليل من التميز." + : "Your next adventure, with a little extra."); + + return Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + categoryName.toText24( + isBold: true, + color: const Color(0xFF202F31), + ), + const SizedBox(height: 6), + subtitle.toText12(color: const Color(0xFF7A8180)), + ], + ), + ), + const SizedBox(width: 16), + _buildCategoryHeroImage( + selectedCategory, + isLatest: isLatest, + isExclusive: isExclusiveMode, + ), + ], + ), + ); + } + + Widget _buildCategoryHeroImage( + WalaPlusCategoryModel? category, { + bool isLatest = false, + bool isExclusive = false, + }) { + const double imgWidth = 84.0; + const double imgHeight = 84.0; + const double radius = 18.0; + + if (isExclusive) { + return _buildCategoryHeroFallback( + category, + imgWidth, + imgHeight, + radius, + isExclusive: true, + ); + } + + if (isLatest) { + return _buildCategoryHeroFallback( + category, + imgWidth, + imgHeight, + radius, + isLatest: true, + ); + } + + String? imgUrl = category?.categoryImageUrl; + if (imgUrl != null && imgUrl.isNotEmpty) { + if (imgUrl.toLowerCase().endsWith('.svg') || (category?.isSvg ?? false)) { + return ClipRRect( + borderRadius: BorderRadius.circular(radius), + child: SizedBox( + width: imgWidth, + height: imgHeight, + child: SvgPicture.network( + imgUrl, + fit: BoxFit.cover, + placeholderBuilder: + (_) => _buildCategoryHeroFallback( + category, + imgWidth, + imgHeight, + radius, + ), + ), + ), + ); + } + return ClipRRect( + borderRadius: BorderRadius.circular(radius), + child: SizedBox( + width: imgWidth, + height: imgHeight, + child: Image.network( + imgUrl, + fit: BoxFit.cover, + errorBuilder: + (_, __, ___) => _buildCategoryHeroFallback( + category, + imgWidth, + imgHeight, + radius, + ), + ), + ), + ); + } + return _buildCategoryHeroFallback(category, imgWidth, imgHeight, radius); + } + + Widget _buildCategoryHeroFallback( + WalaPlusCategoryModel? category, + double w, + double h, + double r, { + bool isLatest = false, + bool isExclusive = false, + }) { + return ClipRRect( + borderRadius: BorderRadius.circular(r), + child: Container( + width: w, + height: h, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: + isExclusive + ? const [Color(0xFF143F36), Color(0xFF0B2A23)] + : isLatest + ? const [Color(0xFF1FA98D), Color(0xFF147D6B)] + : const [Color(0xFF38BDF8), Color(0xFF0284C7)], + ), + ), + child: Center( + child: Icon( + isExclusive + ? Icons.workspace_premium_rounded + : isLatest + ? Icons.local_offer_rounded + : Icons.flight_takeoff_rounded, + size: 36, + color: isExclusive ? const Color(0xFFDCC58F) : Colors.white, + ), + ), + ), + ); + } + + Widget _buildSearchBar(bool isAr) { + return Container( + width: double.infinity, + height: 46, + padding: const EdgeInsets.symmetric(horizontal: 14), + alignment: Alignment.center, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: const Color(0xFFE8EAE6), width: 1.0), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Icon(Icons.search_rounded, size: 20, color: Color(0xFF79807F)), + const SizedBox(width: 10), + Expanded( + child: TextField( + controller: _searchController, + focusNode: _searchFocusNode, + onChanged: _onSearchChanged, + onSubmitted: (String val) { + _debounceTimer?.cancel(); + fetchOffers(query: val.trim()); + }, + textInputAction: TextInputAction.search, + textAlignVertical: TextAlignVertical.center, + style: const TextStyle( + color: Color(0xFF202F31), + fontSize: 13, + fontWeight: FontWeight.w400, + ), + decoration: InputDecoration( + isDense: true, + contentPadding: EdgeInsets.zero, + hintText: LocaleKeys.findAnEscapeOrAnExperience.tr(), + hintStyle: const TextStyle( + color: Color(0xFF7A8180), + fontSize: 13, + fontWeight: FontWeight.w400, + ), + border: InputBorder.none, + ), + ), + ), + ValueListenableBuilder( + valueListenable: _searchController, + builder: ( + BuildContext context, + TextEditingValue value, + Widget? child, + ) { + if (value.text.isEmpty) return const SizedBox.shrink(); + return InkWell( + borderRadius: BorderRadius.circular(16), + onTap: () { + _searchController.clear(); + _onSearchChanged(""); + }, + child: const Padding( + padding: EdgeInsets.all(4), + child: Icon(Icons.clear, size: 18, color: Color(0xFF848486)), + ), + ); + }, + ), + ], + ), + ); + } + + int _activeFilterCount() { + int count = 0; + if (selectedTypeFilter != 'all') count++; + if (_filterSortBy != 'newest') count++; + if (_filterDistance != 'any') count++; + if (_filterMinDiscount != 'any') count++; + return count; + } + + Widget _buildSubcategoriesAndFilterRow(bool isAr) { + bool showSubcategoriesRow = + currentCategoryId.isNotEmpty && + currentCategoryId != "all" && + (isSubcategoriesLoading || subcategoriesList.isNotEmpty); + int activeFilterCount = _activeFilterCount(); + + return Padding( + padding: const EdgeInsets.only(top: 14, bottom: 2), + child: SizedBox( + height: 38, + child: Row( + children: [ + Stack( + clipBehavior: Clip.none, + children: [ + InkWell( + borderRadius: BorderRadius.circular(6), + onTap: _showFiltersBottomSheet, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 14, + vertical: 8, + ), + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(6), + color: MyColors.lightGreyEAColor, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + SvgPicture.asset( + "assets/icons/filter_icon.svg", + width: 14, + height: 14, + colorFilter: const ColorFilter.mode( + MyColors.black, + BlendMode.srcIn, + ), + ), + const SizedBox(width: 5), + LocaleKeys.filters.tr().toText12(color: MyColors.black), + ], + ), + ), + ), + if (activeFilterCount > 0) + PositionedDirectional( + top: -5, + end: -5, + child: Container( + width: 18, + height: 18, + alignment: Alignment.center, + decoration: const BoxDecoration( + color: MyColors.darkIconColor, + shape: BoxShape.circle, + ), + child: '$activeFilterCount'.toText10( + isBold: true, + color: Colors.white, + ), + ), + ), + ], + ), + if (showSubcategoriesRow) ...[ + Container( + margin: const EdgeInsets.symmetric(horizontal: 10), + width: 1, + height: 20, + color: const Color(0xFFE2E8F0), + ), + Expanded( + child: + isSubcategoriesLoading + ? Shimmer.fromColors( + baseColor: const Color(0xFFE2E8F0), + highlightColor: const Color(0xFFF8FAFC), + child: ListView.separated( + scrollDirection: Axis.horizontal, + physics: const NeverScrollableScrollPhysics(), + itemCount: 4, + separatorBuilder: + (BuildContext _, int __) => + const SizedBox(width: 8), + itemBuilder: + (BuildContext _, int __) => Container( + width: 70, + height: 32, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(6), + ), + ), + ), + ) + : ListView.separated( + scrollDirection: Axis.horizontal, + physics: const BouncingScrollPhysics(), + itemCount: subcategoriesList.length + 1, + separatorBuilder: + (BuildContext _, int __) => + const SizedBox(width: 8), + itemBuilder: (BuildContext context, int index) { + bool isAllChip = index == 0; + WalaPlusCategoryModel? subcategory = + isAllChip ? null : subcategoriesList[index - 1]; + bool isSelected = + isAllChip + ? _selectedSubcategoryId == null + : _selectedSubcategoryId == subcategory!.id; + String label = + isAllChip + ? LocaleKeys.allCategories.tr() + : (subcategory!.name ?? ""); + + return InkWell( + borderRadius: BorderRadius.circular(6), + onTap: () { + setState(() { + _selectedSubcategoryId = + isAllChip + ? null + : (isSelected + ? null + : subcategory!.id); + }); + fetchOffers( + categoryId: + _selectedSubcategoryId ?? + currentCategoryId, + ); + }, + 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: label.toText12( + color: + isSelected + ? MyColors.white + : MyColors.black, + ), + ), + ); + }, + ), + ), + ], + ], + ), + ), + ); + } + + void _showCitySelectionBottomSheet() { + showMazayaCitySelectionBottomSheet( + context, + selectedCityId: selectedCity?.cityId, + onCitySelected: (city) { + WalaPlusCityModel? newCity = city.isAllCities ? null : city; + AppState().setSelectedMazayaCity = newCity; + if (newCity != null) { + Utils.saveStringFromPrefs( + "MAZAYA_SELECTED_CITY", + jsonEncode(newCity.toJson()), + ); + } else { + Utils.removeStringFromPrefs("MAZAYA_SELECTED_CITY"); + } + setState(() => selectedCity = newCity); + String effectiveCategoryId = + _selectedSubcategoryId ?? + (currentCategoryId.isEmpty + ? (selectedCategory?.id ?? "") + : currentCategoryId); + fetchOffers( + categoryId: effectiveCategoryId, + query: _searchController.text, + ); + }, + ); + } + + // Matches the shared Filters bottom sheet design. All four sections are fully wired to the + // API: Availability -> WP_Offer_Type (via selectedTypeFilter), Sort by -> WP_Sort_By, Distance + // -> WP_Radius, Minimum discount -> WP_Min_Discount. "Any"/"All" maps to null (param omitted). + void _showFiltersBottomSheet() { + String draftAvailability = selectedTypeFilter; + String draftSortBy = _filterSortBy; + String draftDistance = _filterDistance; + String draftMinDiscount = _filterMinDiscount; + + const Color darkBg = Color(0xFF202B33); + const Color borderColor = Color(0xFFE2E8F0); + + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (BuildContext sheetContext) { + return StatefulBuilder( + builder: (BuildContext context, StateSetter setSheetState) { + Widget buildOption( + String label, + bool isSelected, + VoidCallback onTap, { + IconData? icon, + }) { + return Expanded( + child: InkWell( + borderRadius: BorderRadius.circular(8), + onTap: onTap, + child: Container( + height: 44, + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: + isSelected ? MyColors.lightGreyEAColor : Colors.white, + border: Border.all( + color: + isSelected + ? MyColors.lightGreyEAColor + : borderColor, + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (icon != null) ...[ + Icon( + icon, + size: 14, + color: + isSelected + ? MyColors.black + : const Color(0xFF64748B), + ), + const SizedBox(width: 5), + ], + Flexible( + child: label.toText13( + isBold: true, + color: isSelected ? MyColors.black : darkBg, + maxlines: 1, + ), + ), + if (isSelected) ...[ + const SizedBox(width: 5), + const Icon( + Icons.check, + size: 14, + color: MyColors.black, + ), + ], + ], + ), + ), + ), + ); + } + + Widget sectionHeader(String title, {String? hint}) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + title.toText14(isBold: true, color: darkBg), + if (hint != null) + hint.toText12(color: const Color(0xFF94A3B8)), + ], + ); + } + + return SafeArea( + top: false, + child: Container( + padding: const EdgeInsets.fromLTRB(20, 12, 20, 20), + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(24)), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center( + child: Container( + width: 44, + height: 4.5, + margin: const EdgeInsets.only(bottom: 16), + decoration: BoxDecoration( + color: const Color(0xFFCBD5E1), + borderRadius: BorderRadius.circular(2.5), + ), + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + LocaleKeys.filters.tr().toText20( + isBold: true, + color: darkBg, + ), + InkWell( + borderRadius: BorderRadius.circular(16), + onTap: () => Navigator.pop(sheetContext), + child: Container( + width: 32, + height: 32, + alignment: Alignment.center, + decoration: const BoxDecoration( + shape: BoxShape.circle, + color: Color(0xFFF1F5F9), + ), + child: const Icon( + Icons.close, + size: 18, + color: darkBg, + ), + ), + ), + ], + ), + 20.height, + + // Availability - single select, drives the real offerType filter. + LocaleKeys.availability.tr().toText14( + isBold: true, + color: darkBg, + ), + 10.height, + Row( + children: [ + buildOption( + LocaleKeys.allCategories.tr(), + draftAvailability == 'all', + () => setSheetState(() => draftAvailability = 'all'), + ), + const SizedBox(width: 10), + buildOption( + LocaleKeys.online.tr(), + draftAvailability == 'online', + () => + setSheetState(() => draftAvailability = 'online'), + ), + const SizedBox(width: 10), + buildOption( + LocaleKeys.inStore.tr(), + draftAvailability == 'instore', + () => setSheetState( + () => draftAvailability = 'instore', + ), + ), + ], + ), + 20.height, + + // Sort by - single select, Nearest and Newest only. + LocaleKeys.sortBy.tr().toText14( + isBold: true, + color: darkBg, + ), + 10.height, + Row( + children: [ + buildOption( + LocaleKeys.nearest.tr(), + draftSortBy == 'nearest', + () => setSheetState(() => draftSortBy = 'nearest'), + icon: Icons.location_on_outlined, + ), + const SizedBox(width: 10), + buildOption( + LocaleKeys.newest.tr(), + draftSortBy == 'newest', + () => setSheetState(() => draftSortBy = 'newest'), + ), + ], + ), + 20.height, + + // Distance - single select. + sectionHeader( + LocaleKeys.distance.tr(), + hint: LocaleKeys.fromYourCurrentLocation.tr(), + ), + 10.height, + Row( + children: [ + buildOption( + LocaleKeys.any.tr(), + draftDistance == 'any', + () => setSheetState(() => draftDistance = 'any'), + ), + const SizedBox(width: 8), + buildOption( + LocaleKeys.distanceKm.tr(args: ['5']), + draftDistance == '5km', + () => setSheetState(() => draftDistance = '5km'), + ), + const SizedBox(width: 8), + buildOption( + LocaleKeys.distanceKm.tr(args: ['10']), + draftDistance == '10km', + () => setSheetState(() => draftDistance = '10km'), + ), + const SizedBox(width: 8), + buildOption( + LocaleKeys.distanceKm.tr(args: ['25']), + draftDistance == '25km', + () => setSheetState(() => draftDistance = '25km'), + ), + ], + ), + 20.height, + + // Minimum discount - single select. + LocaleKeys.minimumDiscount.tr().toText14( + isBold: true, + color: darkBg, + ), + 10.height, + Row( + children: [ + buildOption( + LocaleKeys.any.tr(), + draftMinDiscount == 'any', + () => setSheetState(() => draftMinDiscount = 'any'), + ), + const SizedBox(width: 8), + buildOption( + LocaleKeys.discountPercentPlus.tr( + args: ['10'], + ), + draftMinDiscount == '10', + () => setSheetState(() => draftMinDiscount = '10'), + ), + const SizedBox(width: 8), + buildOption( + LocaleKeys.discountPercentPlus.tr( + args: ['20'], + ), + draftMinDiscount == '20', + () => setSheetState(() => draftMinDiscount = '20'), + ), + const SizedBox(width: 8), + buildOption( + LocaleKeys.discountPercentPlus.tr( + args: ['30'], + ), + draftMinDiscount == '30', + () => setSheetState(() => draftMinDiscount = '30'), + ), + ], + ), + 20.height, + const Divider(height: 1, thickness: 1, color: borderColor), + 16.height, + + Row( + children: [ + Expanded( + child: DefaultButton( + LocaleKeys.reset.tr(), + () { + setSheetState(() { + draftAvailability = 'all'; + draftSortBy = 'newest'; + draftDistance = 'any'; + draftMinDiscount = 'any'; + }); + setState(() { + selectedTypeFilter = draftAvailability; + _filterSortBy = draftSortBy; + _filterDistance = draftDistance; + _filterMinDiscount = draftMinDiscount; + }); + Navigator.pop(sheetContext); + fetchOffers( + categoryId: + currentCategoryId.isEmpty + ? "all" + : currentCategoryId, + ); + }, + colors: const [Colors.white, Colors.white], + border: Border.all(color: borderColor), + textColor: darkBg, + ), + ), + const SizedBox(width: 12), + Expanded( + child: DefaultButton( + LocaleKeys.applyFilters.tr(), + () { + setState(() { + selectedTypeFilter = draftAvailability; + _filterSortBy = draftSortBy; + _filterDistance = draftDistance; + _filterMinDiscount = draftMinDiscount; + }); + Navigator.pop(sheetContext); + fetchOffers( + categoryId: + _selectedSubcategoryId ?? currentCategoryId, + ); + }, + ), + ), + ], + ), + ], + ), + ), + ); + }, + ); + }, + ); + } + + void showFavoritesBottomSheet() async { + await showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + elevation: 0, + builder: (BuildContext sheetContext) { + return FavoriteMerchantsBottomSheet(categoriesList: categoriesList); + }, + ); + if (mounted) { + setState(() {}); + } + } +} + +class WalaPlusRetailerCard extends StatelessWidget { + final WalaPlusRetailerModel item; + final bool isAr; + final VoidCallback onTap; + final VoidCallback? onFavoriteTap; + + const WalaPlusRetailerCard({ + Key? key, + required this.item, + required this.isAr, + required this.onTap, + this.onFavoriteTap, + }) : super(key: key); + + static String getDiscountBadge(WalaPlusRetailerModel item) { + if (item.offerTitle != null) { + RegExp percentRegex = RegExp(r'(\d+%)'); + Match? match = percentRegex.firstMatch(item.offerTitle!); + if (match != null) { + return match.group(1)!; + } + } + if (item.name != null) { + RegExp percentRegex = RegExp(r'(\d+%)'); + Match? match = percentRegex.firstMatch(item.name!); + if (match != null) { + return match.group(1)!; + } + } + return ""; + } + + static String getTypeFilterTitle(String type) { + if (type == 'all') { + return LocaleKeys.allCategories.tr(); + } else if (type == 'instore') { + return LocaleKeys.inStore.tr(); + } else if (type == 'online') { + return LocaleKeys.online.tr(); + } + return type.isNotEmpty ? (type[0].toUpperCase() + type.substring(1)) : type; + } + + static IconData getTypeFilterIcon(String type) { + if (type == 'instore') { + return Icons.storefront_rounded; + } else if (type == 'online') { + return Icons.language_rounded; + } + return Icons.tune_rounded; + } + + @override + Widget build(BuildContext context) { + String? bannerUrl = + (item.imageThumb != null && item.imageThumb!.isNotEmpty) + ? item.imageThumb + : item.image; + + String discountText = getDiscountBadge(item); + + String offerSubtitle = ""; + if (discountText.isNotEmpty) { + if (item.offerTitle != null && + item.offerTitle!.toLowerCase().contains("off")) { + offerSubtitle = item.offerTitle!; + } else { + offerSubtitle = LocaleKeys.offDiscount.tr(args: [discountText]); + } + } else if (item.offerTitle != null && item.offerTitle!.trim().isNotEmpty) { + offerSubtitle = item.offerTitle!.trim(); + } + + List offerTypes = item.normalizedTypes; + String usageText = ""; + if (item.displayUsage.isNotEmpty) { + usageText = item.displayUsage + .replaceAll('usage:', LocaleKeys.usageLabel.tr()) + .replaceAll('Usage :', LocaleKeys.usageLabel.tr()); + } + String cityText = item.getDisplayCity(isAr: isAr); + if (cityText.trim().isEmpty) { + cityText = item.displayCity; + } + bool hasChips = + offerTypes.isNotEmpty || + cityText.trim().isNotEmpty || + usageText.trim().isNotEmpty; + + return InkWell( + borderRadius: BorderRadius.circular(18), + onTap: onTap, + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(18), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.08), + blurRadius: 24, + spreadRadius: 0, + ), + ], + ), + padding: const EdgeInsets.all(10), + child: SingleChildScrollView( + physics: const NeverScrollableScrollPhysics(), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Figma redesign (node 927:496): shorter image, no bottom gradient/logo overlay + // (brand moved into the card body below), no discount ribbon - the percentage now + // lives inline in the teal offer subtitle text instead. Image height trimmed from + // 120->110 (not Figma's 82) and padding/gaps left untouched on purpose: this card's + // overall size is mainAxisExtent-driven (see the grid's gridDelegate) and was + // already tuned against real devices - copying Figma's absolute pixels here would + // reintroduce the overflow/empty-space problems that tuning fixed. + ClipRRect( + borderRadius: BorderRadius.circular(14), + child: SizedBox( + height: 110, + width: double.infinity, + child: Stack( + fit: StackFit.expand, + children: [ + (bannerUrl != null && bannerUrl.isNotEmpty) + ? Image.network( + bannerUrl, + fit: BoxFit.cover, + errorBuilder: + (_, __, ___) => Container( + color: const Color(0xFFF1F5F9), + child: const Icon( + Icons.image_outlined, + color: Color(0xFF94A3B8), + size: 32, + ), + ), + ) + : Container( + color: const Color(0xFFF1F5F9), + child: const Icon( + Icons.image_outlined, + color: Color(0xFF94A3B8), + size: 32, + ), + ), + // Exclusive offers (mapped from the legacy Offers & Discounts API) have no + // real WalaPlus retailer id, so there's nothing for add/removeWalaPlusFavorite + // to act on - hide the heart rather than wiring it to a call that can't work. + if (item.isExclusive != true) + PositionedDirectional( + top: 7, + end: 7, + child: InkWell( + borderRadius: BorderRadius.circular(16), + onTap: onFavoriteTap, + child: Container( + width: 27, + height: 27, + decoration: BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.12), + blurRadius: 5, + offset: const Offset(0, 2), + ), + ], + ), + alignment: Alignment.center, + child: Icon( + item.isFavorite == true + ? Icons.favorite_rounded + : Icons.favorite_border_rounded, + size: 15, + color: + item.isFavorite == true + ? Colors.black + : const Color(0xFF1E293B), + ), + ), + ), + ), + if (item.isExclusive == true) + PositionedDirectional( + bottom: 7, + start: 8, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 7, + vertical: 4, + ), + decoration: BoxDecoration( + color: const Color(0xFF143F36), + borderRadius: BorderRadius.circular(6), + ), + child: LocaleKeys.mazayaExclusive.tr().toText10( + color: const Color(0xFFF1EBDD), + ), + ), + ), + ], + ), + ), + ), + const SizedBox(height: 8), + // Brand name + "view details" affordance arrow, replacing the old image-overlay name. + Row( + children: [ + Expanded( + child: (item.name ?? "").toText13( + isBold: true, + maxlines: 1, + overflow: TextOverflow.ellipsis, + color: const Color(0xFF202F32), + ), + ), + const SizedBox(width: 4), + const Icon( + Icons.north_east_rounded, + size: 16, + color: Color(0xFF147D6B), + ), + ], + ), + const SizedBox(height: 2), + offerSubtitle.toText14( + maxlines: 1, + overflow: TextOverflow.ellipsis, + isBold: true, + color: const Color(0xFF147D6B), + ), + if (hasChips) ...[ + const SizedBox(height: 6), + // Fully-rounded pill badges (no icon, no border) matching the new design - type + // chips in blue (In-store) / green (Online), city in grey. + SizedBox( + height: 28, + child: ListView( + scrollDirection: Axis.horizontal, + physics: const BouncingScrollPhysics(), + children: [ + for (String offerType in offerTypes) ...[ + InfoChipWidget( + text: getTypeFilterTitle(offerType), + backgroundColor: + offerType == 'online' + ? const Color(0xFFEAF3EE) + : const Color(0x14062CFF), + textColor: + offerType == 'online' + ? const Color(0xFF147D6B) + : const Color(0xFF2A56B6), + borderColor: Colors.transparent, + borderWidth: 0, + isBold: false, + horizontalPadding: 10, + verticalPadding: 6, + borderRadius: 99, + ), + const SizedBox(width: 5), + ], + if (cityText.trim().isNotEmpty) ...[ + InfoChipWidget( + text: cityText, + backgroundColor: const Color(0xFFEFF2EF), + textColor: const Color(0xFF464746), + borderColor: Colors.transparent, + borderWidth: 0, + isBold: false, + horizontalPadding: 10, + verticalPadding: 6, + borderRadius: 99, + ), + const SizedBox(width: 5), + ], + if (usageText.trim().isNotEmpty) + InfoChipWidget( + text: usageText, + backgroundColor: const Color(0xFFFEF3C7), + textColor: const Color(0xFFB45309), + borderColor: Colors.transparent, + borderWidth: 0, + isBold: false, + horizontalPadding: 10, + verticalPadding: 6, + borderRadius: 99, + ), + ], + ), + ), + ], + ], + ), + ), + ), + ); + } +} + +class FavoriteMerchantsBottomSheet extends StatefulWidget { + final List? categoriesList; + + const FavoriteMerchantsBottomSheet({Key? key, this.categoriesList}) + : super(key: key); + + @override + State createState() => + _FavoriteMerchantsBottomSheetState(); +} + +class _FavoriteMerchantsBottomSheetState + extends State { + List favoriteRetailers = []; + bool isLoading = true; + bool isLoadingMore = false; + bool hasMore = true; + bool _isFetchingPage = false; + int currentPage = 1; + int totalPages = 1; + final int pageSize = 10; + String searchQuery = ""; + Timer? _debounceTimer; + final TextEditingController _searchController = TextEditingController(); + final ScrollController _scrollController = ScrollController(); + + @override + void initState() { + super.initState(); + _scrollController.addListener(_scrollListener); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + fetchFavorites(); + } + }); + } + + @override + void dispose() { + _debounceTimer?.cancel(); + _scrollController.removeListener(_scrollListener); + _scrollController.dispose(); + _searchController.dispose(); + super.dispose(); + } + + void _scrollListener() { + if (!_scrollController.hasClients) return; + double maxScroll = _scrollController.position.maxScrollExtent; + double currentScroll = _scrollController.position.pixels; + if (currentScroll >= maxScroll - 35) { + if (!isLoading && !isLoadingMore && !_isFetchingPage && hasMore) { + fetchNextPage(); + } + } + } + + String? _getUserReferenceForFavorites() { + String ref = OffersAndDiscountsApiClient().resolveUserReference(); + if (ref.trim().isNotEmpty) return ref.trim(); + String? userName = AppState().getUserName; + if (userName != null && userName.trim().isNotEmpty) return userName.trim(); + return null; + } + + Future fetchFavorites({String? query}) async { + if (query != null) { + searchQuery = query; + } + + setState(() { + isLoading = true; + currentPage = 1; + hasMore = true; + }); + + try { + String? queryParam = + searchQuery.trim().isNotEmpty ? searchQuery.trim() : null; + String? userRef = _getUserReferenceForFavorites(); + debugPrint( + "Fetching favorite retailers: query='$queryParam', userReference='$userRef', page=1, per_page=$pageSize", + ); + + WalaPlusRetailersResponse? response = await OffersAndDiscountsApiClient() + .getWalaPlusRetailers( + categoryId: "", + page: 1, + perPage: pageSize, + offerType: null, + query: queryParam, + sortBy: null, + radius: null, + minDiscount: null, + context: null, + userReference: userRef, + ); + + favoriteRetailers = response?.retailers ?? []; + for (WalaPlusRetailerModel r in favoriteRetailers) { + r.isFavorite = true; + } + currentPage = 1; + totalPages = response?.pagesCount ?? 1; + hasMore = + currentPage < totalPages && + (response?.retailers?.isNotEmpty ?? false); + } catch (e) { + debugPrint("Error fetching favorite retailers: $e"); + favoriteRetailers = []; + } finally { + if (mounted) { + setState(() { + isLoading = false; + }); + } + } + } + + Future fetchNextPage() async { + if (isLoading || isLoadingMore || _isFetchingPage || !hasMore) return; + + _isFetchingPage = true; + setState(() { + isLoadingMore = true; + }); + + try { + int nextPage = currentPage + 1; + String? queryParam = + searchQuery.trim().isNotEmpty ? searchQuery.trim() : null; + String? userRef = _getUserReferenceForFavorites(); + + debugPrint( + "Fetching favorite retailers next page: $nextPage of $totalPages, query: '$queryParam', userReference='$userRef'", + ); + + WalaPlusRetailersResponse? response = await OffersAndDiscountsApiClient() + .getWalaPlusRetailers( + categoryId: "", + page: nextPage, + perPage: pageSize, + offerType: null, + query: queryParam, + sortBy: null, + radius: null, + minDiscount: null, + context: null, + userReference: userRef, + ); + + List newRetailers = + response?.retailers ?? []; + if (newRetailers.isNotEmpty) { + for (WalaPlusRetailerModel r in newRetailers) { + r.isFavorite = true; + } + currentPage = nextPage; + totalPages = response?.pagesCount ?? totalPages; + hasMore = currentPage < totalPages; + favoriteRetailers.addAll(newRetailers); + } else { + hasMore = false; + } + } catch (e) { + debugPrint("Error fetching next page of favorite retailers: $e"); + } finally { + _isFetchingPage = false; + if (mounted) { + setState(() { + isLoadingMore = false; + }); + } + } + } + + void _onSearchChanged(String query) { + _debounceTimer?.cancel(); + searchQuery = query; + if (query.trim().isEmpty) { + fetchFavorites(query: ""); + } else { + _debounceTimer = Timer(const Duration(milliseconds: 600), () { + fetchFavorites(query: query.trim()); + }); + } + } + + Future _toggleFavorite(WalaPlusRetailerModel item) async { + String retailerId = item.id ?? ""; + if (retailerId.isEmpty) return; + + bool currentFav = item.isFavorite ?? true; + bool targetFav = !currentFav; + + setState(() { + item.isFavorite = targetFav; + if (!targetFav) { + favoriteRetailers.removeWhere((r) => r.id == item.id); + } + }); + Utils.showLoading(context); + + try { + GenericResponseModel? response; + String? userRef = _getUserReferenceForFavorites(); + if (targetFav) { + response = await OffersAndDiscountsApiClient().addWalaPlusFavorite( + retailerId: retailerId, + context: context, + userReference: userRef, + ); + } else { + response = await OffersAndDiscountsApiClient().removeWalaPlusFavorite( + retailerId: retailerId, + context: context, + userReference: userRef, + ); + } + + Utils.hideLoading(context); + + if (response != null && response.errorMessage == null) { + bool isAr = AppState().isArabic(context); + String msg = + isAr + ? (targetFav + ? LocaleKeys.favoriteAddedSuccess.tr() + : LocaleKeys.favoriteRemovedSuccess.tr()) + : (response.wpFavoritesAddRemoveMessage ?? + (targetFav + ? LocaleKeys.favoriteAddedSuccess.tr() + : LocaleKeys.favoriteRemovedSuccess.tr())); + Utils.showToast(msg, isSuccess: true); + } else { + Utils.showToast( + response?.errorEndUserMessage ?? + response?.errorMessage ?? + LocaleKeys.favoriteError.tr(), + ); + await fetchFavorites(query: _searchController.text); + } + } catch (e) { + Utils.hideLoading(context); + debugPrint("Error toggling favorite in bottom sheet: $e"); + Utils.showToast(LocaleKeys.favoriteError.tr()); + await fetchFavorites(query: _searchController.text); + } + } + + void _openRetailer(WalaPlusRetailerModel item) async { + if ((item.categories == null || item.categories!.isEmpty) && + widget.categoriesList != null && + item.categoryId != null) { + for (var c in widget.categoriesList!) { + if (c.id?.toString() == item.categoryId?.toString()) { + item.categories = [c]; + break; + } + } + } + await Navigator.pushNamed( + context, + AppRoutes.retailerDetailsScreen, + arguments: item, + ); + if (mounted) { + await fetchFavorites(query: _searchController.text); + } + } + + @override + Widget build(BuildContext context) { + bool isAr = AppState().isArabic(context); + + return Container( + height: MediaQuery.of(context).size.height * 0.85, + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(24)), + ), + child: SafeArea( + top: false, + child: Padding( + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).viewInsets.bottom, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center( + child: Container( + width: 44, + height: 4.5, + margin: const EdgeInsets.only(top: 12, bottom: 8), + decoration: BoxDecoration( + color: const Color(0xFFCBD5E1), + borderRadius: BorderRadius.circular(2.5), + ), + ), + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + LocaleKeys.favoriteOffers.tr().toText18( + weight: FontWeight.w600, + color: const Color(0xFF1E293B), + ), + ], + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + InkWell( + borderRadius: BorderRadius.circular(16), + onTap: () => Navigator.pop(context), + child: Container( + width: 32, + height: 32, + alignment: Alignment.center, + decoration: const BoxDecoration( + shape: BoxShape.circle, + color: Color(0xFFF1F5F9), + ), + child: const Icon( + Icons.close_rounded, + size: 18, + color: Color(0xFF1E293B), + ), + ), + ), + ], + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + child: Container( + width: double.infinity, + height: 70, + padding: const EdgeInsets.only( + top: 14, + right: 16, + bottom: 14, + left: 16, + ), + decoration: BoxDecoration( + color: const Color(0xFFFFFFFF), + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: const Color(0xFFE8E8E8), + width: 1.0, + ), + boxShadow: [ + BoxShadow( + color: const Color(0xFF000000).withOpacity(0.0314), + offset: const Offset(0, 2), + blurRadius: 5, + spreadRadius: 0, + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + LocaleKeys.search.tr().toText12( + isBold: true, + color: const Color(0xFF2A333C), + ), + const SizedBox(height: 2), + TextField( + controller: _searchController, + onChanged: _onSearchChanged, + onSubmitted: (String val) { + _debounceTimer?.cancel(); + fetchFavorites(query: val.trim()); + }, + textInputAction: TextInputAction.search, + decoration: InputDecoration( + isDense: true, + contentPadding: EdgeInsets.zero, + hintText: LocaleKeys.searchItems.tr(), + hintStyle: const TextStyle( + color: Color(0xFFACAFB1), + fontSize: 14, + fontWeight: FontWeight.w400, + height: 1.2, + ), + border: InputBorder.none, + suffixIcon: ValueListenableBuilder( + valueListenable: _searchController, + builder: ( + BuildContext context, + TextEditingValue value, + Widget? child, + ) { + if (value.text.isEmpty) + return const SizedBox.shrink(); + return InkWell( + child: const Icon( + Icons.clear, + size: 18, + color: Color(0xFF848486), + ), + onTap: () { + _searchController.clear(); + _onSearchChanged(""); + }, + ); + }, + ), + suffixIconConstraints: const BoxConstraints( + maxWidth: 24, + maxHeight: 28, + ), + ), + style: const TextStyle( + color: Color(0xFF2A333C), + fontSize: 14, + fontWeight: FontWeight.w400, + ), + ), + ], + ), + ), + ), + Expanded( + child: RefreshIndicator( + onRefresh: + () => fetchFavorites(query: _searchController.text), + color: MyColors.textMixColor, + child: SingleChildScrollView( + controller: _scrollController, + physics: const AlwaysScrollableScrollPhysics( + parent: BouncingScrollPhysics(), + ), + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + child: Column( + children: [ + WalaPlusRetailerGrid( + isLoading: isLoading, + retailers: favoriteRetailers, + isAr: isAr, + isLoadingMore: isLoadingMore, + onTapRetailer: _openRetailer, + onFavoriteTap: _toggleFavorite, + ), + const SizedBox(height: 20), + ], + ), + ), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/ui/mazaya/saved_retailers_screen.dart b/lib/ui/mazaya/saved_retailers_screen.dart new file mode 100644 index 0000000..073b906 --- /dev/null +++ b/lib/ui/mazaya/saved_retailers_screen.dart @@ -0,0 +1,390 @@ +import 'dart:async'; + +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:mohem_flutter_app/api/offers_and_discounts_api_client.dart'; +import 'package:mohem_flutter_app/app_state/app_state.dart'; +import 'package:mohem_flutter_app/classes/colors.dart'; +import 'package:mohem_flutter_app/classes/consts.dart'; +import 'package:mohem_flutter_app/classes/utils.dart'; +import 'package:mohem_flutter_app/config/routes.dart'; +import 'package:mohem_flutter_app/extensions/string_extensions.dart'; +import 'package:mohem_flutter_app/generated/locale_keys.g.dart'; +import 'package:mohem_flutter_app/models/generic_response_model.dart'; +import 'package:mohem_flutter_app/models/offers_and_discounts/wala_plus_retailer_model.dart'; +import 'package:mohem_flutter_app/ui/mazaya/widgets/mazaya_bottom_nav.dart'; +import 'package:mohem_flutter_app/ui/mazaya/widgets/wala_plus_retailer_grid.dart'; +import 'package:mohem_flutter_app/widgets/app_bar_widget.dart'; + +// Mazaya Saved tab - full-screen version of what was FavoriteMerchantsBottomSheet (a modal +// bottom sheet). Same fetch/search/pagination/toggle-favorite logic, now its own pushed route +// with the shared MazayaBottomNav instead of a dismissible sheet - matches Discover's "own +// route, fetches fresh on mount" pattern. +class SavedRetailersScreen extends StatefulWidget { + const SavedRetailersScreen({Key? key}) : super(key: key); + + @override + State createState() => _SavedRetailersScreenState(); +} + +class _SavedRetailersScreenState extends State { + List favoriteRetailers = []; + bool isLoading = true; + bool isLoadingMore = false; + bool hasMore = true; + bool _isFetchingPage = false; + int currentPage = 1; + int totalPages = 1; + final int pageSize = 10; + String searchQuery = ""; + Timer? _debounceTimer; + final TextEditingController _searchController = TextEditingController(); + final ScrollController _scrollController = ScrollController(); + + @override + void initState() { + super.initState(); + _scrollController.addListener(_scrollListener); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) fetchFavorites(); + }); + } + + @override + void dispose() { + _debounceTimer?.cancel(); + _scrollController.removeListener(_scrollListener); + _scrollController.dispose(); + _searchController.dispose(); + super.dispose(); + } + + void _scrollListener() { + if (!_scrollController.hasClients) return; + double maxScroll = _scrollController.position.maxScrollExtent; + double currentScroll = _scrollController.position.pixels; + if (currentScroll >= maxScroll - 35) { + if (!isLoading && !isLoadingMore && !_isFetchingPage && hasMore) { + fetchNextPage(); + } + } + } + + String? _getUserReferenceForFavorites() { + String ref = OffersAndDiscountsApiClient().resolveUserReference(); + if (ref.trim().isNotEmpty) return ref.trim(); + String? userName = AppState().getUserName; + if (userName != null && userName.trim().isNotEmpty) return userName.trim(); + return null; + } + + Future fetchFavorites({String? query}) async { + if (query != null) searchQuery = query; + setState(() { + isLoading = true; + currentPage = 1; + hasMore = true; + }); + try { + String? queryParam = + searchQuery.trim().isNotEmpty ? searchQuery.trim() : null; + String? userRef = _getUserReferenceForFavorites(); + WalaPlusRetailersResponse? response = await OffersAndDiscountsApiClient() + .getWalaPlusRetailers( + categoryId: "", + page: 1, + perPage: pageSize, + offerType: null, + query: queryParam, + sortBy: null, + radius: null, + minDiscount: null, + context: null, + userReference: userRef, + ); + favoriteRetailers = response?.retailers ?? []; + for (WalaPlusRetailerModel r in favoriteRetailers) { + r.isFavorite = true; + } + currentPage = 1; + totalPages = response?.pagesCount ?? 1; + hasMore = + currentPage < totalPages && + (response?.retailers?.isNotEmpty ?? false); + } catch (e) { + debugPrint("MazayaSavedScreen: error fetching favorites: $e"); + favoriteRetailers = []; + } finally { + if (mounted) setState(() => isLoading = false); + } + } + + Future fetchNextPage() async { + if (isLoading || isLoadingMore || _isFetchingPage || !hasMore) return; + _isFetchingPage = true; + setState(() => isLoadingMore = true); + try { + int nextPage = currentPage + 1; + String? queryParam = + searchQuery.trim().isNotEmpty ? searchQuery.trim() : null; + String? userRef = _getUserReferenceForFavorites(); + WalaPlusRetailersResponse? response = await OffersAndDiscountsApiClient() + .getWalaPlusRetailers( + categoryId: "", + page: nextPage, + perPage: pageSize, + offerType: null, + query: queryParam, + sortBy: null, + radius: null, + minDiscount: null, + context: null, + userReference: userRef, + ); + List newRetailers = + response?.retailers ?? []; + if (newRetailers.isNotEmpty) { + for (WalaPlusRetailerModel r in newRetailers) { + r.isFavorite = true; + } + currentPage = nextPage; + totalPages = response?.pagesCount ?? totalPages; + hasMore = currentPage < totalPages; + favoriteRetailers.addAll(newRetailers); + } else { + hasMore = false; + } + } catch (e) { + debugPrint("MazayaSavedScreen: error fetching next page: $e"); + } finally { + _isFetchingPage = false; + if (mounted) setState(() => isLoadingMore = false); + } + } + + void _onSearchChanged(String query) { + _debounceTimer?.cancel(); + searchQuery = query; + if (query.trim().isEmpty) { + fetchFavorites(query: ""); + } else { + _debounceTimer = Timer(const Duration(milliseconds: 600), () { + fetchFavorites(query: query.trim()); + }); + } + } + + Future _toggleFavorite(WalaPlusRetailerModel item) async { + String retailerId = item.id ?? ""; + if (retailerId.isEmpty) return; + + bool currentFav = item.isFavorite ?? true; + bool targetFav = !currentFav; + + setState(() { + item.isFavorite = targetFav; + if (!targetFav) { + favoriteRetailers.removeWhere( + (WalaPlusRetailerModel r) => r.id == item.id, + ); + } + }); + Utils.showLoading(context); + + try { + GenericResponseModel? response; + String? userRef = _getUserReferenceForFavorites(); + if (targetFav) { + response = await OffersAndDiscountsApiClient().addWalaPlusFavorite( + retailerId: retailerId, + context: context, + userReference: userRef, + ); + } else { + response = await OffersAndDiscountsApiClient().removeWalaPlusFavorite( + retailerId: retailerId, + context: context, + userReference: userRef, + ); + } + + Utils.hideLoading(context); + + if (response != null && response.errorMessage == null) { + bool isAr = AppState().isArabic(context); + String msg = + isAr + ? (targetFav + ? LocaleKeys.favoriteAddedSuccess.tr() + : LocaleKeys.favoriteRemovedSuccess.tr()) + : (response.wpFavoritesAddRemoveMessage ?? + (targetFav + ? LocaleKeys.favoriteAddedSuccess.tr() + : LocaleKeys.favoriteRemovedSuccess.tr())); + Utils.showToast(msg, isSuccess: true); + } else { + Utils.showToast( + response?.errorEndUserMessage ?? + response?.errorMessage ?? + LocaleKeys.favoriteError.tr(), + ); + await fetchFavorites(query: _searchController.text); + } + } catch (e) { + Utils.hideLoading(context); + debugPrint("MazayaSavedScreen: error toggling favorite: $e"); + Utils.showToast(LocaleKeys.favoriteError.tr()); + await fetchFavorites(query: _searchController.text); + } + } + + void _openRetailer(WalaPlusRetailerModel item) async { + await Navigator.pushNamed( + context, + AppRoutes.retailerDetailsScreen, + arguments: item, + ); + if (mounted) { + await fetchFavorites(query: _searchController.text); + } + } + + @override + Widget build(BuildContext context) { + bool isAr = AppState().isArabic(context); + return Scaffold( + backgroundColor: Colors.white, + appBar: AppBarWidget( + context, + title: LocaleKeys.favoriteOffers.tr(), + showHomeButton: false, + // Reached from the Mazaya bottom nav - same as Discover, no back button for a + // dependent-login session since this is one of their root tabs, not a pushed screen. + showBackButton: false, + ), + bottomNavigationBar: const MazayaBottomNav(currentTab: MazayaTab.saved), + body: Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Container( + width: double.infinity, + height: 70, + padding: const EdgeInsets.only( + top: 14, + right: 16, + bottom: 14, + left: 16, + ), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: const Color(0xFFE8E8E8), width: 1.0), + boxShadow: [ + BoxShadow( + color: const Color(0xFF000000).withOpacity(0.0314), + offset: const Offset(0, 2), + blurRadius: 5, + spreadRadius: 0, + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + LocaleKeys.search.tr().toText12( + isBold: true, + color: const Color(0xFF2A333C), + ), + const SizedBox(height: 2), + TextField( + controller: _searchController, + onChanged: _onSearchChanged, + onSubmitted: (String val) { + _debounceTimer?.cancel(); + fetchFavorites(query: val.trim()); + }, + textInputAction: TextInputAction.search, + decoration: InputDecoration( + isDense: true, + contentPadding: EdgeInsets.zero, + hintText: LocaleKeys.searchItems.tr(), + hintStyle: const TextStyle( + color: Color(0xFFACAFB1), + fontSize: 14, + fontWeight: FontWeight.w400, + height: 1.2, + ), + border: InputBorder.none, + suffixIcon: ValueListenableBuilder( + valueListenable: _searchController, + builder: ( + BuildContext context, + TextEditingValue value, + Widget? child, + ) { + if (value.text.isEmpty) + return const SizedBox.shrink(); + return InkWell( + child: const Icon( + Icons.clear, + size: 18, + color: Color(0xFF848486), + ), + onTap: () { + _searchController.clear(); + _onSearchChanged(""); + }, + ); + }, + ), + suffixIconConstraints: const BoxConstraints( + maxWidth: 24, + maxHeight: 28, + ), + ), + style: const TextStyle( + color: Color(0xFF2A333C), + fontSize: 14, + fontWeight: FontWeight.w400, + ), + ), + ], + ), + ), + ), + Expanded( + child: RefreshIndicator( + onRefresh: () => fetchFavorites(query: _searchController.text), + color: MyColors.textMixColor, + child: SingleChildScrollView( + controller: _scrollController, + physics: const AlwaysScrollableScrollPhysics( + parent: BouncingScrollPhysics(), + ), + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + child: Column( + children: [ + WalaPlusRetailerGrid( + isLoading: isLoading, + retailers: favoriteRetailers, + isAr: isAr, + isLoadingMore: isLoadingMore, + onTapRetailer: _openRetailer, + onFavoriteTap: _toggleFavorite, + ), + const SizedBox(height: 20), + ], + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/ui/mazaya/widgets/mazaya_bottom_nav.dart b/lib/ui/mazaya/widgets/mazaya_bottom_nav.dart new file mode 100644 index 0000000..898478a --- /dev/null +++ b/lib/ui/mazaya/widgets/mazaya_bottom_nav.dart @@ -0,0 +1,130 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:mohem_flutter_app/classes/consts.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/ui/mazaya/mazaya_discover_screen.dart'; +import 'package:mohem_flutter_app/ui/mazaya/saved_retailers_screen.dart'; +import 'package:mohem_flutter_app/ui/profile/family_members.dart'; + +enum MazayaTab { discover, saved, myFamily } + +// Shared bottom nav for the Mazaya module (Discover / Saved / My Family). Each tab is its own +// screen, swapped in with zero-duration transition (see _onTabTapped) so switching tabs feels +// instant rather than like a new screen opening - not an IndexedStack shell, since each tab still +// needs to reload its own data fresh on every switch, not preserve state across tabs. +class MazayaBottomNav extends StatelessWidget { + final MazayaTab currentTab; + + const MazayaBottomNav({Key? key, required this.currentTab}) : super(key: key); + + static const Color _activeColor = Color(0xFF147D6B); + static const Color _inactiveColor = Color(0xFF78817F); + + void _onTabTapped(BuildContext context, MazayaTab tab) { + if (tab == currentTab) return; + Widget targetScreen; + RouteSettings? settings; + switch (tab) { + case MazayaTab.discover: + targetScreen = const MazayaDiscoverScreen(); + break; + case MazayaTab.saved: + targetScreen = const SavedRetailersScreen(); + break; + case MazayaTab.myFamily: + targetScreen = const FamilyMembers(); + settings = const RouteSettings( + arguments: {'fromMazaya': true}, + ); + break; + } + Navigator.pushReplacement( + context, + PageRouteBuilder( + settings: settings, + pageBuilder: + ( + BuildContext context, + Animation animation, + Animation secondaryAnimation, + ) => targetScreen, + transitionDuration: Duration.zero, + reverseTransitionDuration: Duration.zero, + ), + ); + } + + Widget _buildItem( + BuildContext context, { + required MazayaTab tab, + required String svgAsset, + required String label, + }) { + bool isActive = tab == currentTab; + return Expanded( + child: InkWell( + onTap: () => _onTabTapped(context, tab), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SvgPicture.asset( + svgAsset, + width: 22, + height: 22, + colorFilter: ColorFilter.mode( + isActive ? _activeColor : _inactiveColor, + BlendMode.srcIn, + ), + ), + const SizedBox(height: 5), + label.toText11( + weight: isActive ? FontWeight.w600 : FontWeight.w400, + color: isActive ? _activeColor : _inactiveColor, + ), + ], + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + return SafeArea( + top: false, + child: Container( + decoration: const BoxDecoration( + color: Colors.white, + border: Border(top: BorderSide(color: Color(0xFFE8EAE4))), + ), + padding: const EdgeInsets.only(top: 13, bottom: 9), + child: Row( + children: [ + _buildItem( + context, + tab: MazayaTab.discover, + svgAsset: "assets/icons/mazaya_compass.svg", + label: LocaleKeys.discover.tr(), + ), + _buildItem( + context, + tab: MazayaTab.saved, + svgAsset: "assets/icons/mazaya_heart_outline.svg", + label: LocaleKeys.saved.tr(), + ), + // My Family lets the account holder manage their dependents' Mazaya access - not + // applicable (and not shown) when a dependent is the one logged in. + if (!ApiConsts.isDependetLogin) + _buildItem( + context, + tab: MazayaTab.myFamily, + svgAsset: "assets/icons/mazaya_user_round.svg", + label: LocaleKeys.myFamily.tr(), + ), + ], + ), + ), + ); + } +} diff --git a/lib/ui/mazaya/widgets/mazaya_city_selector.dart b/lib/ui/mazaya/widgets/mazaya_city_selector.dart new file mode 100644 index 0000000..41fff78 --- /dev/null +++ b/lib/ui/mazaya/widgets/mazaya_city_selector.dart @@ -0,0 +1,196 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:mohem_flutter_app/api/offers_and_discounts_api_client.dart'; +import 'package:mohem_flutter_app/app_state/app_state.dart'; +import 'package:mohem_flutter_app/extensions/string_extensions.dart'; +import 'package:mohem_flutter_app/generated/locale_keys.g.dart'; +import 'package:mohem_flutter_app/models/offers_and_discounts/wala_plus_city_model.dart'; +import 'package:shimmer/shimmer.dart'; + +// Shared by MazayaDiscoverScreen and RetailersListScreen so both city pickers stay +// visually/behaviorally identical - change the sheet design here only. Fetched fresh from +// getWalaPlusCities every time the sheet opens (no caching), per the confirmed plan. +void showMazayaCitySelectionBottomSheet(BuildContext context, {required int? selectedCityId, required ValueChanged onCitySelected}) { + showModalBottomSheet( + context: context, + backgroundColor: Colors.transparent, + isScrollControlled: true, + builder: (BuildContext sheetContext) { + return _MazayaCitySelectionSheet(selectedCityId: selectedCityId, onCitySelected: onCitySelected); + }, + ); +} + +class _MazayaCitySelectionSheet extends StatefulWidget { + final int? selectedCityId; + final ValueChanged onCitySelected; + + const _MazayaCitySelectionSheet({required this.selectedCityId, required this.onCitySelected}); + + @override + State<_MazayaCitySelectionSheet> createState() => _MazayaCitySelectionSheetState(); +} + +class _MazayaCitySelectionSheetState extends State<_MazayaCitySelectionSheet> { + bool _isLoading = true; + List _cities = []; + List _filteredCities = []; + final TextEditingController _searchController = TextEditingController(); + + @override + void initState() { + super.initState(); + _fetchCities(); + } + + @override + void dispose() { + _searchController.dispose(); + super.dispose(); + } + + Future _fetchCities() async { + try { + List cities = await OffersAndDiscountsApiClient().getWalaPlusCities(); + // "All Cities" always first - it's the default selection when nothing else was picked yet. + cities.sort((a, b) { + if (a.isAllCities == b.isAllCities) return 0; + return a.isAllCities ? -1 : 1; + }); + if (mounted) { + setState(() { + _cities = cities; + _filteredCities = cities; + _isLoading = false; + }); + } + } catch (e) { + debugPrint("MazayaCitySelector: error fetching cities: $e"); + if (mounted) setState(() => _isLoading = false); + } + } + + // Local/client-side only - the already-fetched list is small enough that there's no need to + // re-hit getWalaPlusCities per keystroke. + void _onSearchChanged(String query) { + String q = query.trim().toLowerCase(); + setState(() { + _filteredCities = + q.isEmpty + ? _cities + : _cities.where((WalaPlusCityModel c) { + return (c.cityNameEn ?? '').toLowerCase().contains(q) || (c.cityNameAr ?? '').toLowerCase().contains(q); + }).toList(); + }); + } + + @override + Widget build(BuildContext context) { + bool isAr = AppState().isArabic(context); + // Fixed height regardless of result count - a search that narrows the list down to one or + // two cities (or the full unfiltered list) should never make the sheet itself grow/shrink, + // only the list inside it scrolls. + return Container( + height: MediaQuery.of(context).size.height * 0.75, + padding: const EdgeInsets.fromLTRB(16, 12, 16, 24), + decoration: const BoxDecoration(color: Colors.white, borderRadius: BorderRadius.vertical(top: Radius.circular(20))), + child: SafeArea( + top: false, + child: Column( + children: [ + Container(width: 40, height: 4, margin: const EdgeInsets.only(bottom: 12), decoration: BoxDecoration(color: const Color(0xFFCBD5E1), borderRadius: BorderRadius.circular(2))), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + LocaleKeys.selectCity.tr().toText16(isBold: true, color: const Color(0xFF1E293B)), + InkWell( + onTap: () => Navigator.pop(context), + borderRadius: BorderRadius.circular(16), + child: const Padding(padding: EdgeInsets.all(4), child: Icon(Icons.close_rounded, size: 20, color: Color(0xFF64748B))), + ), + ], + ), + const SizedBox(height: 12), + _buildSearchField(), + const SizedBox(height: 12), + Expanded(child: _isLoading ? _buildShimmer() : _buildList(isAr)), + ], + ), + ), + ); + } + + // Same design as MazayaDiscoverScreen's own search field - filters the already-fetched city + // list locally as the user types (see _onSearchChanged). + Widget _buildSearchField() { + return Container( + height: 47, + padding: const EdgeInsets.symmetric(horizontal: 14), + decoration: BoxDecoration(color: Colors.white, border: Border.all(color: const Color(0xFFE8EAE4)), borderRadius: BorderRadius.circular(12)), + child: Row( + children: [ + SvgPicture.asset("assets/icons/mazaya_search.svg", width: 18, height: 18, colorFilter: const ColorFilter.mode(Color(0xFF78817F), BlendMode.srcIn)), + const SizedBox(width: 10), + Expanded( + child: TextField( + controller: _searchController, + onChanged: _onSearchChanged, + decoration: InputDecoration(border: InputBorder.none, isDense: true, hintText: LocaleKeys.searchCities.tr(), hintStyle: const TextStyle(color: Color(0xFF78817F), fontSize: 12)), + style: const TextStyle(fontSize: 12, color: Color(0xFF202F32)), + ), + ), + ], + ), + ); + } + + // Matches ListTile's own default one-line height (56) and contentPadding (16 horizontal) so + // there's no layout jump between the shimmer and the real list underneath it. + static const double _rowHeight = 56; + + Widget _buildShimmer() { + return Shimmer.fromColors( + baseColor: const Color(0xFFE2E8F0), + highlightColor: const Color(0xFFF8FAFC), + child: ListView.builder( + physics: const NeverScrollableScrollPhysics(), + itemCount: 10, + itemBuilder: + (BuildContext _, int __) => Container( + height: _rowHeight, + padding: const EdgeInsets.symmetric(horizontal: 16), + alignment: AlignmentDirectional.centerStart, + child: Container(height: 28, decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(8))), + ), + ), + ); + } + + Widget _buildList(bool isAr) { + if (_filteredCities.isEmpty) { + return Center(child: LocaleKeys.noResultsFound.tr().toText13(color: const Color(0xFF7A8180))); + } + // ListTile paints its background/ink splashes on the nearest Material ancestor - without + // this, the opaque DecoratedBox the whole sheet sits in (see build()) hides them. + return Material( + type: MaterialType.transparency, + child: ListView.builder( + padding: EdgeInsets.zero, + itemCount: _filteredCities.length, + itemBuilder: (BuildContext _, int index) { + WalaPlusCityModel city = _filteredCities[index]; + bool isSelected = widget.selectedCityId != null ? city.cityId == widget.selectedCityId : city.isAllCities; + return ListTile( + title: city.getDisplayName(isAr).toText14(isBold: isSelected, color: isSelected ? const Color(0xFF0D9488) : const Color(0xFF1E293B)), + trailing: isSelected ? const Icon(Icons.check_rounded, color: Color(0xFF0D9488), size: 20) : null, + onTap: () { + widget.onCitySelected(city); + Navigator.pop(context); + }, + ); + }, + ), + ); + } +} diff --git a/lib/ui/mazaya/widgets/wala_plus_retailer_grid.dart b/lib/ui/mazaya/widgets/wala_plus_retailer_grid.dart new file mode 100644 index 0000000..a28a60e --- /dev/null +++ b/lib/ui/mazaya/widgets/wala_plus_retailer_grid.dart @@ -0,0 +1,124 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:mohem_flutter_app/extensions/int_extensions.dart'; +import 'package:mohem_flutter_app/extensions/string_extensions.dart'; +import 'package:mohem_flutter_app/generated/locale_keys.g.dart'; +import 'package:mohem_flutter_app/main.dart'; +import 'package:mohem_flutter_app/models/offers_and_discounts/wala_plus_retailer_model.dart'; +import 'package:mohem_flutter_app/ui/mazaya/retailers_list_screen.dart'; +import 'package:shimmer/shimmer.dart'; + +class WalaPlusRetailerGrid extends StatelessWidget { + final bool isLoading; + final List retailers; + final bool isAr; + final void Function(WalaPlusRetailerModel item) onTapRetailer; + final void Function(WalaPlusRetailerModel item) onFavoriteTap; + final bool isLoadingMore; + final int shimmerItemCount; + final double? mainAxisExtent; + + const WalaPlusRetailerGrid({ + Key? key, + required this.isLoading, + required this.retailers, + required this.isAr, + required this.onTapRetailer, + required this.onFavoriteTap, + this.isLoadingMore = false, + this.shimmerItemCount = 4, + this.mainAxisExtent, + }) : super(key: key); + + bool get _hasAnyChips { + for (WalaPlusRetailerModel r in retailers) { + if (r.normalizedTypes.isNotEmpty || + r.displayCity.trim().isNotEmpty || + r.displayUsage.trim().isNotEmpty) { + return true; + } + } + return false; + } + + double get _effectiveMainAxisExtent { + if (mainAxisExtent != null) return mainAxisExtent!; + if (retailers.isNotEmpty && !_hasAnyChips) return 190; + return 235; + } + + SliverGridDelegateWithFixedCrossAxisCount get _gridDelegate => + SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: isTablet ? 4 : 2, + mainAxisExtent: _effectiveMainAxisExtent, + crossAxisSpacing: 12, + mainAxisSpacing: 16, + ); + + @override + Widget build(BuildContext context) { + if (isLoading) { + return Shimmer.fromColors( + baseColor: const Color(0xFFE2E8F0), + highlightColor: const Color(0xFFF8FAFC), + child: GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: _gridDelegate, + itemCount: shimmerItemCount, + itemBuilder: (BuildContext _, int __) => _buildShimmerCard(), + ), + ); + } + + if (retailers.isEmpty) { + return Padding(padding: const EdgeInsets.only(top: 40), child: Center(child: LocaleKeys.noOffersAvailable.tr().toText14(color: const Color(0xFF64748B)))); + } + + return Column( + children: [ + GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: _gridDelegate, + itemCount: retailers.length, + itemBuilder: (BuildContext context, int index) { + return WalaPlusRetailerCard(item: retailers[index], isAr: isAr, onTap: () => onTapRetailer(retailers[index]), onFavoriteTap: () => onFavoriteTap(retailers[index])); + }, + ), + if (isLoadingMore) ...[ + 16.height, + Center(child: Padding(padding: const EdgeInsets.symmetric(vertical: 12), child: Image.asset("assets/images/logos/loading_mohemm_logo.gif", height: 48.0, width: 48.0))), + ], + ], + ); + } + + Widget _buildShimmerCard() { + bool showChips = _effectiveMainAxisExtent > 200; + return Container( + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20)), + padding: const EdgeInsets.all(10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container(height: 110, width: double.infinity, decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(14))), + const SizedBox(height: 8), + Container(height: 14, width: 110, decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(4))), + const SizedBox(height: 6), + Container(height: 14, width: 80, decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(4))), + if (showChips) ...[ + const SizedBox(height: 6), + Row( + children: [ + Container(height: 28, width: 50, decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(6))), + const SizedBox(width: 5), + Container(height: 28, width: 45, decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(6))), + ], + ), + ], + ], + ), + ); + } +} diff --git a/lib/ui/misc/request_submit_screen.dart b/lib/ui/misc/request_submit_screen.dart index ceba8be..472e3c6 100644 --- a/lib/ui/misc/request_submit_screen.dart +++ b/lib/ui/misc/request_submit_screen.dart @@ -21,7 +21,7 @@ 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/button/simple_button.dart'; import 'package:mohem_flutter_app/widgets/circular_avatar.dart'; -import 'package:mohem_flutter_app/widgets/dialogs/confirm_dialog.dart'; +import 'package:mohem_flutter_app/widgets/bottom_sheets/confirm_bottom_sheet.dart'; import 'package:mohem_flutter_app/widgets/image_picker.dart'; import 'package:mohem_flutter_app/widgets/input_widget.dart'; @@ -145,7 +145,7 @@ class _RequestSubmitScreenState extends State { ); } else {} Utils.hideLoading(context); - Utils.showToast(LocaleKeys.yourRequestHasBeenSubmittedForApprovals.tr(), longDuration: true); + Utils.showToast(LocaleKeys.yourRequestHasBeenSubmittedForApprovals.tr(), longDuration: true, isSuccess: true); AppState().cancelRequestTrancsection = false; if (params?.popNavigateToSpecificRoute == null) { @@ -216,15 +216,14 @@ class _RequestSubmitScreenState extends State { return; } - showDialog( - context: context, - builder: (cxt) => ConfirmDialog( - message: LocaleKeys.areYouSureYouWantToSubmit.tr(), - onTap: () { - Navigator.pop(context); - submitRequest(); - }, - ), + ConfirmBottomSheet.show( + context, + message: LocaleKeys.areYouSureYouWantToSubmit.tr(), + confirmText: LocaleKeys.yes.tr(), + cancelText: LocaleKeys.no.tr(), + onConfirm: () { + submitRequest(); + }, ); }, ).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/my_team/employee_details.dart b/lib/ui/my_team/employee_details.dart index e78c304..87172ac 100644 --- a/lib/ui/my_team/employee_details.dart +++ b/lib/ui/my_team/employee_details.dart @@ -18,6 +18,7 @@ import 'package:mohem_flutter_app/models/my_team/get_employee_subordinates_list. import 'package:mohem_flutter_app/models/profile_menu.model.dart'; import 'package:mohem_flutter_app/models/worklist/get_favorite_replacements_model.dart'; import 'package:mohem_flutter_app/models/worklist/replacement_list_model.dart'; +import 'package:mohem_flutter_app/widgets/bottom_sheets/confirm_bottom_sheet.dart'; import 'package:mohem_flutter_app/widgets/circular_avatar.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -217,19 +218,19 @@ class _EmployeeDetailsState extends State { } void showFavoriteAlertDialog(BuildContext context) { - Widget cancelButton = TextButton( - child: Text( - LocaleKeys.cancel.tr(), - ), - onPressed: () { - Navigator.pop(context); - }, - ); - Widget continueButton = TextButton( - child: Text( - LocaleKeys.ok.tr(), - ), - onPressed: () { + ConfirmBottomSheet.show( + context, + message: LocaleKeys.addFavoriteList.tr(namedArgs: {'name': '${getEmployeeSubordinates!.eMPLOYEENAME}'}), + confirmText: LocaleKeys.yes.tr(), + cancelText: LocaleKeys.no.tr(), + avatar: getEmployeeSubordinates!.eMPLOYEEIMAGE == null + ? SvgPicture.asset("assets/images/user.svg", height: 68, width: 68) + : CircleAvatar( + radius: 68, + backgroundImage: MemoryImage(Utils.dataFromBase64String(getEmployeeSubordinates!.eMPLOYEEIMAGE!)), + backgroundColor: Colors.black, + ), + onConfirm: () { fetchChangeFav( email: getEmployeeSubordinates?.eMPLOYEEEMAILADDRESS ?? "", employeName: getEmployeeSubordinates!.eMPLOYEENAME ?? "", @@ -238,54 +239,6 @@ class _EmployeeDetailsState extends State { isFav: true, ); setState(() {}); - Navigator.pop(context); - }, - ); - AlertDialog alert = AlertDialog( - title: Text( - LocaleKeys.confirm.tr(), - ), - content: Container( - height: 150, - child: Column( - children: [ - Text(LocaleKeys.addFavoriteList).tr(namedArgs: {'name': '${getEmployeeSubordinates!.eMPLOYEENAME}'}), - getEmployeeSubordinates!.eMPLOYEEIMAGE == null - ? SvgPicture.asset( - "assets/images/user.svg", - height: 68, - width: 68, - ) - : CircleAvatar( - radius: 68, - backgroundImage: MemoryImage(Utils.dataFromBase64String(getEmployeeSubordinates!.eMPLOYEEIMAGE!)), - backgroundColor: Colors.black, - ), - // Image.memory( - // Utils.getPostBytes( - // getEmployeeSubordinates!.eMPLOYEEIMAGE ?? "", - // ), - // errorBuilder: (BuildContext context, error, stackTrace) { - // return SvgPicture.asset( - // "assets/images/user.svg", height: 50, width: 50, - // ); - // }, - // width: 50, - // height: 50, - // fit: BoxFit.cover, - // ).circle(50).paddingOnly(top: 21), - ], - ), - ), - actions: [ - cancelButton, - continueButton, - ], - ); - showDialog( - context: context, - builder: (BuildContext context) { - return alert; }, ); } diff --git a/lib/ui/parking_qr/parking_qr.dart b/lib/ui/parking_qr/parking_qr.dart index d9e86f5..08d7b10 100644 --- a/lib/ui/parking_qr/parking_qr.dart +++ b/lib/ui/parking_qr/parking_qr.dart @@ -49,11 +49,11 @@ class _RequestDetailsState extends State { children: [ // QR Code Image if (qrData!.getEmployeeParking!.qrCodeImageBase64 != null && qrData!.getEmployeeParking!.qrCodeImageBase64!.isNotEmpty) - Image.memory(Utils.dataFromBase64String(qrData!.getEmployeeParking!.qrCodeImageBase64!), width: 300, height: 300, fit: BoxFit.contain) + Image.memory(Utils.dataFromBase64String(qrData!.getEmployeeParking!.qrCodeImageBase64!), width: 150, height: 150, fit: BoxFit.contain) else Container( - width: 300, - height: 300, + width: 200, + height: 200, color: MyColors.lightGreyEFColor, child: Center(child: Text(LocaleKeys.noDataAvailable.tr(), style: const TextStyle(color: MyColors.grey67Color))), ), 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..8fb72a3 --- /dev/null +++ b/lib/ui/profile/add_dependent_verify_screen.dart @@ -0,0 +1,2415 @@ +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 = {}; + + // Track field keys/prompts that were prefilled from patientInfo + final Set _prefilledFieldKeys = {}; + + bool _isFieldPrefilledFromPatientInfo(String? colName, String? prompt, String? val) { + if (_patientInfo == null) return false; + if (val == null || val.trim().isEmpty) return false; + if (colName != null && _prefilledFieldKeys.contains(colName.toUpperCase().trim())) { + return true; + } + if (prompt != null && _prefilledFieldKeys.contains(prompt.toLowerCase().trim())) { + return true; + } + return false; + } + + IconData? _getSuffixIconForField(String colName, String prompt, String dataType) { + String col = colName.toUpperCase(); + String p = prompt.toLowerCase(); + if (col.contains('EMAIL') || p.contains('email') || p.contains('بريد')) { + return Icons.mail_outline_rounded; + } + if (col.contains('PHONE') || col.contains('MOBILE') || col.contains('TEL') || p.contains('phone') || p.contains('mobile') || p.contains('جوال') || p.contains('هاتف')) { + return Icons.phone_outlined; + } + if (dataType == 'DATE' || col.contains('DATE') || p.contains('date') || p.contains('تاريخ')) { + return Icons.calendar_today_outlined; + } + return null; + } + + @override + void initState() { + super.initState(); + _nationalIdController.addListener(_onNationalIdChanged); + _fetchEbsStructures(); + } + + void _onNationalIdChanged() { + if (mounted) setState(() {}); + } + + void _onFieldControllerChanged() { + if (mounted) setState(() {}); + } + + @override + void dispose() { + _nationalIdController.removeListener(_onNationalIdChanged); + _nationalIdController.dispose(); + for (var c in _fieldControllers.values) { + c.removeListener(_onFieldControllerChanged); + c.dispose(); + } + _fieldControllers.clear(); + super.dispose(); + } + + bool _isStep1Valid() { + String natId = _nationalIdController.text.trim(); + if (natId.isEmpty) return false; + if (_isLegislationSA && natId.length != 10) return false; + if (_selectedDob == null) return false; + return true; + } + + bool _isStep2Valid() { + for (int i = 0; i < _contactColsStructureList.length; i++) { + var col = _contactColsStructureList[i]; + if (col.dISPLAYFLAG != "N" && col.rEQUIREDFLAG == "Y") { + String colKey = col.aPPLICATIONCOLUMNNAME ?? "COL_$i"; + String textVal = _fieldControllers[colKey]?.text.trim() ?? ""; + String dspVal = col.getContactDetailsList?.sEGMENTVALUEDSP?.trim() ?? ""; + String codeVal = col.getContactDetailsList?.vARCHAR2VALUE?.trim() ?? ""; + String dateVal = col.getContactDetailsList?.dATEVALUE?.trim() ?? ""; + if (textVal.isEmpty && dspVal.isEmpty && codeVal.isEmpty && dateVal.isEmpty) { + return false; + } + } + } + for (int i = 0; i < _basicDetDffStructureList.length; i++) { + var basic = _basicDetDffStructureList[i]; + if (basic.dISPLAYFLAG != "N" && basic.rEQUIREDFLAG == "Y") { + String colKey = basic.aPPLICATIONCOLUMNNAME ?? "BASIC_DFF_$i"; + String textVal = _fieldControllers[colKey]?.text.trim() ?? ""; + String dspVal = basic.getContactDetailsList?.sEGMENTVALUEDSP?.trim() ?? ""; + String codeVal = basic.getContactDetailsList?.vARCHAR2VALUE?.trim() ?? ""; + String dateVal = basic.getContactDetailsList?.dATEVALUE?.trim() ?? ""; + if (textVal.isEmpty && dspVal.isEmpty && codeVal.isEmpty && dateVal.isEmpty) { + return false; + } + } + } + return true; + } + + bool _isStep3Valid() { + for (int i = 0; i < _contactDffStructureList.length; i++) { + var dff = _contactDffStructureList[i]; + if (dff.dISPLAYFLAG != "N" && dff.rEQUIREDFLAG == "Y") { + String colKey = dff.aPPLICATIONCOLUMNNAME ?? "CONTACT_DFF_$i"; + String textVal = _fieldControllers[colKey]?.text.trim() ?? ""; + String dspVal = dff.getContactDetailsList?.sEGMENTVALUEDSP?.trim() ?? ""; + String codeVal = dff.getContactDetailsList?.vARCHAR2VALUE?.trim() ?? ""; + String dateVal = dff.getContactDetailsList?.dATEVALUE?.trim() ?? ""; + if (textVal.isEmpty && dspVal.isEmpty && codeVal.isEmpty && dateVal.isEmpty) { + return false; + } + } + } + return true; + } + + TextEditingController _getControllerForField(String key, String? initialValue) { + if (!_fieldControllers.containsKey(key)) { + TextEditingController ctrl = TextEditingController(text: initialValue ?? ''); + ctrl.addListener(_onFieldControllerChanged); + _fieldControllers[key] = ctrl; + } + return _fieldControllers[key]!; + } + + void _syncController(String key, String? value) { + if (_fieldControllers.containsKey(key)) { + if (_fieldControllers[key]!.text != (value ?? '')) { + _fieldControllers[key]!.text = value ?? ''; + } + } else { + TextEditingController ctrl = TextEditingController(text: value ?? ''); + ctrl.addListener(_onFieldControllerChanged); + _fieldControllers[key] = ctrl; + } + } + + 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); + if (_patientInfo != null) { + if (colName.isNotEmpty) _prefilledFieldKeys.add(colName); + if (prompt.isNotEmpty) { + _prefilledFieldKeys.add(prompt); + _prefilledFieldKeys.add(prompt.replaceAll('*', '').trim()); + } + if (colKey.isNotEmpty) _prefilledFieldKeys.add(colKey); + } + } + // 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); + if (_patientInfo != null) { + if (colName.isNotEmpty) _prefilledFieldKeys.add(colName); + if (prompt.isNotEmpty) { + _prefilledFieldKeys.add(prompt); + _prefilledFieldKeys.add(prompt.replaceAll('*', '').trim()); + } + if (colKey.isNotEmpty) _prefilledFieldKeys.add(colKey); + } + } + } + } + + _contactColsStructureList.forEach(matchStep1); + _basicDetDffStructureList.forEach(matchStep1); + _contactDffStructureList.forEach(matchStep1); + } + + // --- Map GetPatientInfo to Dynamic Structures --- + void _applyPrefilledPatientInfo(PatientInfoModel patient, bool isAr, String nationalId, String dob) { + _prefilledFieldKeys.clear(); + + 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 ?? ''; + + void markPrefilled() { + if (colName.isNotEmpty) _prefilledFieldKeys.add(colName); + if (prompt.isNotEmpty) { + _prefilledFieldKeys.add(prompt); + _prefilledFieldKeys.add(prompt.replaceAll('*', '').trim()); + } + if (colKey.isNotEmpty) _prefilledFieldKeys.add(colKey); + } + + // 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); + markPrefilled(); + } + // 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); + markPrefilled(); + } + } + // 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); + markPrefilled(); + } + } + // 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); + markPrefilled(); + } + } + // 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); + markPrefilled(); + } + } + // 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); + markPrefilled(); + } + } + // 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); + markPrefilled(); + } + } + 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'; + } + markPrefilled(); + } + // 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; + markPrefilled(); + 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; + markPrefilled(); + break; + } + } + } else if (eServices != null && eServices.isNotEmpty) { + for (var opt in eServices) { + String code = (opt.iDCOLUMNNAME ?? '').trim().toUpperCase(); + String meaning = (opt.vALUECOLUMNNAME ?? '').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.vALUECOLUMNNAME; + details.vARCHAR2VALUE = opt.iDCOLUMNNAME; + markPrefilled(); + break; + } else if (isMale && (code == 'MR' || (!meaning.contains('mrs') && !meaning.contains('سيدة') && (meaning.startsWith('mr') || meaning.contains('mr.') || meaning.contains('سيد'))))) { + details.sEGMENTVALUEDSP = opt.vALUECOLUMNNAME; + details.vARCHAR2VALUE = opt.iDCOLUMNNAME; + markPrefilled(); + 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; + markPrefilled(); + break; + } else if (isMale && (code == 'SON' || (!meaning.contains('daughter') && (meaning.contains('son') || meaning.contains('ابن') || meaning.contains('ولد'))))) { + details.sEGMENTVALUEDSP = opt.mEANING; + details.vARCHAR2VALUE = opt.cODE; + markPrefilled(); + break; + } + } + } else if (eServices != null && eServices.isNotEmpty) { + for (var opt in eServices) { + String code = (opt.iDCOLUMNNAME ?? '').trim().toUpperCase(); + String meaning = (opt.vALUECOLUMNNAME ?? '').trim().toLowerCase(); + if (isFemale && (code == 'DAUGHTER' || meaning.contains('daughter') || meaning.contains('ابنة') || meaning.contains('بنت'))) { + details.sEGMENTVALUEDSP = opt.vALUECOLUMNNAME; + details.vARCHAR2VALUE = opt.iDCOLUMNNAME; + markPrefilled(); + break; + } else if (isMale && (code == 'SON' || (!meaning.contains('daughter') && (meaning.contains('son') || meaning.contains('ابن') || meaning.contains('ولد'))))) { + details.sEGMENTVALUEDSP = opt.vALUECOLUMNNAME; + details.vARCHAR2VALUE = opt.iDCOLUMNNAME; + markPrefilled(); + 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 == '' || 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; + } + } + + // --- Helper to build text fields styled specifically for Add Dependent Screen --- + Widget _buildFigmaTextField( + String labelText, + String hintText, { + TextEditingController? controller, + VoidCallback? onTap, + IconData? suffixIconData, + bool isEnable = true, + bool isReadOnly = false, + bool isPopup = false, + int? lines = 1, + TextInputAction? inputAction, + void Function(String)? onChange, + bool isInputTypeNum = false, + int? maxLength, + List? inputFormatters, + Color? backgroundColor, + Color? borderColor, + double? borderWidth, + Color? labelColor, + Color? textColor, + Color? suffixIconColor, + }) { + return DynamicTextFieldWidget( + labelText, + hintText, + controller: controller, + onTap: onTap, + suffixIconData: suffixIconData, + isEnable: isEnable, + isReadOnly: isReadOnly, + isPopup: isPopup, + lines: lines, + inputAction: inputAction, + onChange: onChange, + isInputTypeNum: isInputTypeNum, + maxLength: maxLength, + inputFormatters: inputFormatters, + backgroundColor: backgroundColor ?? Colors.white, + borderColor: borderColor, + borderWidth: borderWidth, + labelColor: labelColor, + textColor: textColor, + suffixIconColor: suffixIconColor, + borderRadius: BorderRadius.circular(14), + constraints: const BoxConstraints(minHeight: 66), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + boxShadow: const [ + BoxShadow( + color: Color.fromRGBO(0, 0, 0, 0.0314), + offset: Offset(0, 2), + blurRadius: 5, + spreadRadius: 0, + ), + ], + ); + } + + // --- 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 rawPrompt = (model.sEGMENTPROMPT ?? "").trim(); + String label = rawPrompt.endsWith("*") + ? rawPrompt + : (isRequired ? "$rawPrompt*" : rawPrompt); + model.getContactDetailsList ??= GetContactDetailsList(); + String colName = (model.aPPLICATIONCOLUMNNAME ?? "").toUpperCase().trim(); + String prompt = rawPrompt.toLowerCase(); + String colKey = model.aPPLICATIONCOLUMNNAME ?? "COL_$index"; + + if (model.dATATYPE == "VARCHAR2") { + if (model.objectValuesList != null && model.objectValuesList!.isNotEmpty) { + 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 ?? ""; + bool isFieldDisabled = _isFieldPrefilledFromPatientInfo(colName, prompt, displayVal); + bool hasSelectedValue = displayVal.trim().isNotEmpty; + bool showRedBorder = isRequired && !isFieldDisabled && !hasSelectedValue; + Color effectiveBorderColor = showRedBorder + ? const Color(0xFFED1C2B) + : const Color(0xFFE6E9E8); + double effectiveBorderWidth = showRedBorder ? 1.4 : 1.0; + Color effectiveLabelColor = isFieldDisabled ? const Color(0xFF8E95A2) : const Color(0xff2B353E); + Color effectiveTextColor = isFieldDisabled + ? const Color(0xFF8E95A2) + : (displayVal.isNotEmpty ? const Color(0xff2B353E) : const Color(0xFF94A3B8)); + Color effectiveSuffixColor = isFieldDisabled ? const Color(0xFF8E95A2) : const Color(0xFF64748B); + + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: PopupMenuButton( + enabled: !isFieldDisabled, + child: _buildFigmaTextField( + label, + displayVal.isNotEmpty ? displayVal : LocaleKeys.select.tr(), + isEnable: false, + isPopup: true, + backgroundColor: Colors.white, + borderColor: effectiveBorderColor, + borderWidth: effectiveBorderWidth, + labelColor: effectiveLabelColor, + textColor: effectiveTextColor, + suffixIconColor: effectiveSuffixColor, + ), + itemBuilder: (_) => isFieldDisabled + ? >[] + : >[ + for (int i = 0; i < model.objectValuesList!.length; i++) + PopupMenuItem( + value: i, + child: (model.objectValuesList![i].mEANING ?? "").toText14(), + ), + ], + onSelected: isFieldDisabled + ? null + : (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); + bool isFieldDisabled = _isFieldPrefilledFromPatientInfo(colName, prompt, controller.text); + bool hasValue = controller.text.trim().isNotEmpty || (model.getContactDetailsList?.sEGMENTVALUEDSP?.trim().isNotEmpty ?? false); + bool showRedBorder = isRequired && !isFieldDisabled && !hasValue; + Color effectiveBorderColor = showRedBorder + ? const Color(0xFFED1C2B) + : const Color(0xFFE6E9E8); + double effectiveBorderWidth = showRedBorder ? 1.4 : 1.0; + Color effectiveLabelColor = isFieldDisabled ? const Color(0xFF8E95A2) : const Color(0xff2B353E); + Color effectiveTextColor = isFieldDisabled ? const Color(0xFF8E95A2) : const Color(0xff2B353E); + Color effectiveSuffixColor = isFieldDisabled ? const Color(0xFF8E95A2) : const Color(0xFF64748B); + + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: _buildFigmaTextField( + label, + LocaleKeys.enterFieldPlaceholder.tr(args: [label]), + controller: controller, + isEnable: !isFieldDisabled, + isReadOnly: isFieldDisabled, + backgroundColor: Colors.white, + borderColor: effectiveBorderColor, + borderWidth: effectiveBorderWidth, + labelColor: effectiveLabelColor, + textColor: effectiveTextColor, + suffixIconData: _getSuffixIconForField(colName, prompt, "VARCHAR2"), + suffixIconColor: effectiveSuffixColor, + onChange: isFieldDisabled + ? null + : (String text) { + model.getContactDetailsList!.sEGMENTVALUEDSP = text; + model.getContactDetailsList!.vARCHAR2VALUE = text; + }, + ), + ); + } + } else if (model.dATATYPE == "NUMBER") { + var controller = _getControllerForField(colKey, model.getContactDetailsList!.sEGMENTVALUEDSP); + bool isFieldDisabled = _isFieldPrefilledFromPatientInfo(colName, prompt, controller.text); + bool hasValue = controller.text.trim().isNotEmpty || (model.getContactDetailsList?.sEGMENTVALUEDSP?.trim().isNotEmpty ?? false); + bool showRedBorder = isRequired && !isFieldDisabled && !hasValue; + Color effectiveBorderColor = showRedBorder + ? const Color(0xFFED1C2B) + : const Color(0xFFE6E9E8); + double effectiveBorderWidth = showRedBorder ? 1.4 : 1.0; + Color effectiveLabelColor = isFieldDisabled ? const Color(0xFF8E95A2) : const Color(0xff2B353E); + Color effectiveTextColor = isFieldDisabled ? const Color(0xFF8E95A2) : const Color(0xff2B353E); + Color effectiveSuffixColor = isFieldDisabled ? const Color(0xFF8E95A2) : const Color(0xFF64748B); + + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: _buildFigmaTextField( + label, + LocaleKeys.enterFieldPlaceholder.tr(args: [label]), + controller: controller, + isEnable: !isFieldDisabled, + isReadOnly: isFieldDisabled, + isInputTypeNum: true, + backgroundColor: Colors.white, + borderColor: effectiveBorderColor, + borderWidth: effectiveBorderWidth, + labelColor: effectiveLabelColor, + textColor: effectiveTextColor, + suffixIconData: _getSuffixIconForField(colName, prompt, "NUMBER"), + suffixIconColor: effectiveSuffixColor, + onChange: isFieldDisabled + ? null + : (String text) { + model.getContactDetailsList!.sEGMENTVALUEDSP = text; + model.getContactDetailsList!.vARCHAR2VALUE = text; + }, + ), + ); + } else if (model.dATATYPE == "DATE") { + String displayVal = model.getContactDetailsList!.sEGMENTVALUEDSP ?? ""; + bool isFieldDisabled = _isFieldPrefilledFromPatientInfo(colName, prompt, displayVal); + bool hasSelectedValue = displayVal.trim().isNotEmpty; + bool showRedBorder = isRequired && !isFieldDisabled && !hasSelectedValue; + Color effectiveBorderColor = showRedBorder + ? const Color(0xFFED1C2B) + : const Color(0xFFE6E9E8); + double effectiveBorderWidth = showRedBorder ? 1.4 : 1.0; + Color effectiveLabelColor = isFieldDisabled ? const Color(0xFF8E95A2) : const Color(0xff2B353E); + Color effectiveTextColor = isFieldDisabled + ? const Color(0xFF8E95A2) + : (displayVal.isNotEmpty ? const Color(0xff2B353E) : const Color(0xFF94A3B8)); + Color effectiveSuffixColor = isFieldDisabled ? const Color(0xFF8E95A2) : const Color(0xFF64748B); + + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: _buildFigmaTextField( + label, + displayVal.isNotEmpty ? displayVal : LocaleKeys.dateFormatPlaceholder.tr(), + suffixIconData: Icons.calendar_today_outlined, + isEnable: !isFieldDisabled, + isReadOnly: true, + backgroundColor: Colors.white, + borderColor: effectiveBorderColor, + borderWidth: effectiveBorderWidth, + labelColor: effectiveLabelColor, + textColor: effectiveTextColor, + suffixIconColor: effectiveSuffixColor, + onTap: isFieldDisabled + ? null + : () 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 rawPrompt = (model.sEGMENTPROMPT ?? "").trim(); + String label = rawPrompt.endsWith("*") + ? rawPrompt + : (isRequired ? "$rawPrompt*" : rawPrompt); + model.getContactDetailsList ??= GetContactDetailsList(); + String colName = (model.aPPLICATIONCOLUMNNAME ?? "").toUpperCase().trim(); + String prompt = rawPrompt.toLowerCase(); + String colKey = model.aPPLICATIONCOLUMNNAME ?? "BASIC_DFF_$index"; + + if (model.fORMATTYPE == "C") { + if (model.eSERVICESVS != null && model.eSERVICESVS!.isNotEmpty) { + 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 ?? ""; + bool isFieldDisabled = _isFieldPrefilledFromPatientInfo(colName, prompt, displayVal); + bool hasSelectedValue = displayVal.trim().isNotEmpty; + bool showRedBorder = isRequired && !isFieldDisabled && !hasSelectedValue; + Color effectiveBorderColor = showRedBorder + ? const Color(0xFFED1C2B) + : const Color(0xFFE6E9E8); + double effectiveBorderWidth = showRedBorder ? 1.4 : 1.0; + Color effectiveLabelColor = isFieldDisabled ? const Color(0xFF8E95A2) : const Color(0xff2B353E); + Color effectiveTextColor = isFieldDisabled + ? const Color(0xFF8E95A2) + : (displayVal.isNotEmpty ? const Color(0xff2B353E) : const Color(0xFF94A3B8)); + Color effectiveSuffixColor = isFieldDisabled ? const Color(0xFF8E95A2) : const Color(0xFF64748B); + + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: PopupMenuButton( + enabled: !isFieldDisabled, + child: _buildFigmaTextField( + label, + displayVal.isNotEmpty ? displayVal : LocaleKeys.select.tr(), + isEnable: false, + isPopup: true, + backgroundColor: Colors.white, + borderColor: effectiveBorderColor, + borderWidth: effectiveBorderWidth, + labelColor: effectiveLabelColor, + textColor: effectiveTextColor, + suffixIconColor: effectiveSuffixColor, + ), + itemBuilder: (_) => isFieldDisabled + ? >[] + : >[ + for (int i = 0; i < model.eSERVICESVS!.length; i++) + PopupMenuItem( + value: i, + child: (model.eSERVICESVS![i].vALUECOLUMNNAME ?? "").toText14(), + ), + ], + onSelected: isFieldDisabled + ? null + : (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); + bool isFieldDisabled = _isFieldPrefilledFromPatientInfo(colName, prompt, controller.text); + bool hasValue = controller.text.trim().isNotEmpty || (model.getContactDetailsList?.sEGMENTVALUEDSP?.trim().isNotEmpty ?? false); + bool showRedBorder = isRequired && !isFieldDisabled && !hasValue; + Color effectiveBorderColor = showRedBorder + ? const Color(0xFFED1C2B) + : const Color(0xFFE6E9E8); + double effectiveBorderWidth = showRedBorder ? 1.4 : 1.0; + Color effectiveLabelColor = isFieldDisabled ? const Color(0xFF8E95A2) : const Color(0xff2B353E); + Color effectiveTextColor = isFieldDisabled ? const Color(0xFF8E95A2) : const Color(0xff2B353E); + Color effectiveSuffixColor = isFieldDisabled ? const Color(0xFF8E95A2) : const Color(0xFF64748B); + + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: _buildFigmaTextField( + label, + LocaleKeys.enterFieldPlaceholder.tr(args: [label]), + controller: controller, + isEnable: !isFieldDisabled, + isReadOnly: isFieldDisabled, + backgroundColor: Colors.white, + borderColor: effectiveBorderColor, + borderWidth: effectiveBorderWidth, + labelColor: effectiveLabelColor, + textColor: effectiveTextColor, + suffixIconData: _getSuffixIconForField(colName, prompt, "VARCHAR2"), + suffixIconColor: effectiveSuffixColor, + onChange: isFieldDisabled + ? null + : (String text) { + model.getContactDetailsList!.sEGMENTVALUEDSP = text; + model.getContactDetailsList!.vARCHAR2VALUE = text; + }, + ), + ); + } + } else if (model.fORMATTYPE == "N") { + var controller = _getControllerForField(colKey, model.getContactDetailsList!.sEGMENTVALUEDSP); + bool isFieldDisabled = _isFieldPrefilledFromPatientInfo(colName, prompt, controller.text); + bool hasValue = controller.text.trim().isNotEmpty || (model.getContactDetailsList?.sEGMENTVALUEDSP?.trim().isNotEmpty ?? false); + bool showRedBorder = isRequired && !isFieldDisabled && !hasValue; + Color effectiveBorderColor = showRedBorder + ? const Color(0xFFED1C2B) + : const Color(0xFFE6E9E8); + double effectiveBorderWidth = showRedBorder ? 1.4 : 1.0; + Color effectiveLabelColor = isFieldDisabled ? const Color(0xFF8E95A2) : const Color(0xff2B353E); + Color effectiveTextColor = isFieldDisabled ? const Color(0xFF8E95A2) : const Color(0xff2B353E); + Color effectiveSuffixColor = isFieldDisabled ? const Color(0xFF8E95A2) : const Color(0xFF64748B); + + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: _buildFigmaTextField( + label, + LocaleKeys.enterFieldPlaceholder.tr(args: [label]), + controller: controller, + isEnable: !isFieldDisabled, + isReadOnly: isFieldDisabled, + isInputTypeNum: true, + backgroundColor: Colors.white, + borderColor: effectiveBorderColor, + borderWidth: effectiveBorderWidth, + labelColor: effectiveLabelColor, + textColor: effectiveTextColor, + suffixIconData: _getSuffixIconForField(colName, prompt, "NUMBER"), + suffixIconColor: effectiveSuffixColor, + onChange: isFieldDisabled + ? null + : (String text) { + model.getContactDetailsList!.sEGMENTVALUEDSP = text; + model.getContactDetailsList!.vARCHAR2VALUE = text; + }, + ), + ); + } else if (model.fORMATTYPE == "X") { + String displayVal = model.getContactDetailsList!.sEGMENTVALUEDSP ?? ""; + bool isFieldDisabled = _isFieldPrefilledFromPatientInfo(colName, prompt, displayVal); + bool hasSelectedValue = displayVal.trim().isNotEmpty; + bool showRedBorder = isRequired && !isFieldDisabled && !hasSelectedValue; + Color effectiveBorderColor = showRedBorder + ? const Color(0xFFED1C2B) + : const Color(0xFFE6E9E8); + double effectiveBorderWidth = showRedBorder ? 1.4 : 1.0; + Color effectiveLabelColor = isFieldDisabled ? const Color(0xFF8E95A2) : const Color(0xff2B353E); + Color effectiveTextColor = isFieldDisabled + ? const Color(0xFF8E95A2) + : (displayVal.isNotEmpty ? const Color(0xff2B353E) : const Color(0xFF94A3B8)); + Color effectiveSuffixColor = isFieldDisabled ? const Color(0xFF8E95A2) : const Color(0xFF64748B); + + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: _buildFigmaTextField( + label, + displayVal.isNotEmpty ? displayVal : LocaleKeys.dateFormatPlaceholder.tr(), + suffixIconData: Icons.calendar_today_outlined, + isEnable: !isFieldDisabled, + isReadOnly: true, + backgroundColor: Colors.white, + borderColor: effectiveBorderColor, + borderWidth: effectiveBorderWidth, + labelColor: effectiveLabelColor, + textColor: effectiveTextColor, + suffixIconColor: effectiveSuffixColor, + onTap: isFieldDisabled + ? null + : () 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 rawPrompt = (model.sEGMENTPROMPT ?? "").trim(); + String label = rawPrompt.endsWith("*") + ? rawPrompt + : (isRequired ? "$rawPrompt*" : rawPrompt); + model.getContactDetailsList ??= GetContactDetailsList(); + String colName = (model.aPPLICATIONCOLUMNNAME ?? "").toUpperCase().trim(); + String prompt = rawPrompt.toLowerCase(); + String colKey = model.aPPLICATIONCOLUMNNAME ?? "CONTACT_DFF_$index"; + + if (model.fORMATTYPE == "C") { + if (model.eSERVICESVS != null && model.eSERVICESVS!.isNotEmpty) { + 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 ?? ""; + bool isFieldDisabled = _isFieldPrefilledFromPatientInfo(colName, prompt, displayVal); + bool hasSelectedValue = displayVal.trim().isNotEmpty; + bool showRedBorder = isRequired && !isFieldDisabled && !hasSelectedValue; + Color effectiveBorderColor = showRedBorder + ? const Color(0xFFED1C2B) + : const Color(0xFFE6E9E8); + double effectiveBorderWidth = showRedBorder ? 1.4 : 1.0; + Color effectiveLabelColor = isFieldDisabled ? const Color(0xFF8E95A2) : const Color(0xff2B353E); + Color effectiveTextColor = isFieldDisabled + ? const Color(0xFF8E95A2) + : (displayVal.isNotEmpty ? const Color(0xff2B353E) : const Color(0xFF94A3B8)); + Color effectiveSuffixColor = isFieldDisabled ? const Color(0xFF8E95A2) : const Color(0xFF64748B); + + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: PopupMenuButton( + enabled: !isFieldDisabled, + child: _buildFigmaTextField( + label, + displayVal.isNotEmpty ? displayVal : LocaleKeys.select.tr(), + isEnable: false, + isPopup: true, + backgroundColor: Colors.white, + borderColor: effectiveBorderColor, + borderWidth: effectiveBorderWidth, + labelColor: effectiveLabelColor, + textColor: effectiveTextColor, + suffixIconColor: effectiveSuffixColor, + ), + itemBuilder: (_) => isFieldDisabled + ? >[] + : >[ + for (int i = 0; i < model.eSERVICESVS!.length; i++) + PopupMenuItem( + value: i, + child: (model.eSERVICESVS![i].vALUECOLUMNNAME ?? "").toText14(), + ), + ], + onSelected: isFieldDisabled + ? null + : (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); + bool isFieldDisabled = _isFieldPrefilledFromPatientInfo(colName, prompt, controller.text); + bool hasValue = controller.text.trim().isNotEmpty || (model.getContactDetailsList?.sEGMENTVALUEDSP?.trim().isNotEmpty ?? false); + bool showRedBorder = isRequired && !isFieldDisabled && !hasValue; + Color effectiveBorderColor = showRedBorder + ? const Color(0xFFED1C2B) + : const Color(0xFFE6E9E8); + double effectiveBorderWidth = showRedBorder ? 1.4 : 1.0; + Color effectiveLabelColor = isFieldDisabled ? const Color(0xFF8E95A2) : const Color(0xff2B353E); + Color effectiveTextColor = isFieldDisabled ? const Color(0xFF8E95A2) : const Color(0xff2B353E); + Color effectiveSuffixColor = isFieldDisabled ? const Color(0xFF8E95A2) : const Color(0xFF64748B); + + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: _buildFigmaTextField( + label, + LocaleKeys.enterFieldPlaceholder.tr(args: [label]), + controller: controller, + isEnable: !isFieldDisabled, + isReadOnly: isFieldDisabled, + backgroundColor: Colors.white, + borderColor: effectiveBorderColor, + borderWidth: effectiveBorderWidth, + labelColor: effectiveLabelColor, + textColor: effectiveTextColor, + suffixIconData: _getSuffixIconForField(colName, prompt, "VARCHAR2"), + suffixIconColor: effectiveSuffixColor, + onChange: isFieldDisabled + ? null + : (String text) { + model.getContactDetailsList!.sEGMENTVALUEDSP = text; + model.getContactDetailsList!.vARCHAR2VALUE = text; + }, + ), + ); + } + } else if (model.fORMATTYPE == "N") { + var controller = _getControllerForField(colKey, model.getContactDetailsList!.sEGMENTVALUEDSP); + bool isFieldDisabled = _isFieldPrefilledFromPatientInfo(colName, prompt, controller.text); + bool hasValue = controller.text.trim().isNotEmpty || (model.getContactDetailsList?.sEGMENTVALUEDSP?.trim().isNotEmpty ?? false); + bool showRedBorder = isRequired && !isFieldDisabled && !hasValue; + Color effectiveBorderColor = showRedBorder + ? const Color(0xFFED1C2B) + : const Color(0xFFE6E9E8); + double effectiveBorderWidth = showRedBorder ? 1.4 : 1.0; + Color effectiveLabelColor = isFieldDisabled ? const Color(0xFF8E95A2) : const Color(0xff2B353E); + Color effectiveTextColor = isFieldDisabled ? const Color(0xFF8E95A2) : const Color(0xff2B353E); + Color effectiveSuffixColor = isFieldDisabled ? const Color(0xFF8E95A2) : const Color(0xFF64748B); + + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: _buildFigmaTextField( + label, + LocaleKeys.enterFieldPlaceholder.tr(args: [label]), + controller: controller, + isEnable: !isFieldDisabled, + isReadOnly: isFieldDisabled, + isInputTypeNum: true, + backgroundColor: Colors.white, + borderColor: effectiveBorderColor, + borderWidth: effectiveBorderWidth, + labelColor: effectiveLabelColor, + textColor: effectiveTextColor, + suffixIconData: _getSuffixIconForField(colName, prompt, "NUMBER"), + suffixIconColor: effectiveSuffixColor, + onChange: isFieldDisabled + ? null + : (String text) { + model.getContactDetailsList!.sEGMENTVALUEDSP = text; + model.getContactDetailsList!.vARCHAR2VALUE = text; + }, + ), + ); + } else if (model.fORMATTYPE == "X") { + String displayVal = model.getContactDetailsList?.dATEVALUE ?? model.getContactDetailsList?.sEGMENTVALUEDSP ?? ""; + bool isFieldDisabled = _isFieldPrefilledFromPatientInfo(colName, prompt, displayVal); + bool hasSelectedValue = displayVal.trim().isNotEmpty; + bool showRedBorder = isRequired && !isFieldDisabled && !hasSelectedValue; + Color effectiveBorderColor = showRedBorder + ? const Color(0xFFED1C2B) + : const Color(0xFFE6E9E8); + double effectiveBorderWidth = showRedBorder ? 1.4 : 1.0; + Color effectiveLabelColor = isFieldDisabled ? const Color(0xFF8E95A2) : const Color(0xff2B353E); + Color effectiveTextColor = isFieldDisabled + ? const Color(0xFF8E95A2) + : (displayVal.isNotEmpty ? const Color(0xff2B353E) : const Color(0xFF94A3B8)); + Color effectiveSuffixColor = isFieldDisabled ? const Color(0xFF8E95A2) : const Color(0xFF64748B); + + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: _buildFigmaTextField( + label, + displayVal.isNotEmpty ? displayVal : LocaleKeys.dateFormatPlaceholder.tr(), + suffixIconData: Icons.calendar_today_outlined, + isEnable: !isFieldDisabled, + isReadOnly: true, + backgroundColor: Colors.white, + borderColor: effectiveBorderColor, + borderWidth: effectiveBorderWidth, + labelColor: effectiveLabelColor, + textColor: effectiveTextColor, + suffixIconColor: effectiveSuffixColor, + onTap: isFieldDisabled + ? null + : () async { + DateTime initial = DateTime.now(); + if (displayVal.isNotEmpty) { + try { + initial = DateFormat('yyyy-MM-dd').parse(displayVal); + } catch (_) { + try { + initial = DateFormat('dd/MM/yyyy').parse(displayVal); + } catch (_) {} + } + } + DateTime? picked = await _selectDate(context, initial); + if (picked != null) { + String formatted = DateFormat('yyyy-MM-dd', "en_US").format(picked); + setState(() { + model.getContactDetailsList!.sEGMENTVALUEDSP = formatted; + model.getContactDetailsList!.dATEVALUE = formatted; + model.getContactDetailsList!.vARCHAR2VALUE = formatted; + }); + } + }, + ), + ); + } + return const SizedBox.shrink(); + } + + // --- Step 2 Validation & Next --- + void _onNextFromBasicInfo() { + for (var col in _contactColsStructureList) { + if (col.dISPLAYFLAG != "N" && col.rEQUIREDFLAG == "Y") { + String? val = col.getContactDetailsList?.sEGMENTVALUEDSP; + String? varch = col.getContactDetailsList?.vARCHAR2VALUE; + if ((val == null || val.trim().isEmpty) && (varch == null || varch.trim().isEmpty)) { + String name = col.sEGMENTPROMPT ?? LocaleKeys.requiredField.tr(); + Utils.showToast(LocaleKeys.pleaseFillField.tr(args: [name])); + return; + } + } + } + for (var basic in _basicDetDffStructureList) { + if (basic.dISPLAYFLAG != "N" && basic.rEQUIREDFLAG == "Y") { + String? val = basic.getContactDetailsList?.sEGMENTVALUEDSP; + String? varch = basic.getContactDetailsList?.vARCHAR2VALUE; + if ((val == null || val.trim().isEmpty) && (varch == null || varch.trim().isEmpty)) { + String name = basic.sEGMENTPROMPT ?? LocaleKeys.requiredField.tr(); + Utils.showToast(LocaleKeys.pleaseFillField.tr(args: [name])); + return; + } + } + } + setState(() { + _currentStep = 3; // Move to Step 3: Additional Info + }); + } + + // --- Step 3 Validation & Next --- + void _onNextFromAdditionalInfo() { + for (var dff in _contactDffStructureList) { + if (dff.dISPLAYFLAG != "N" && dff.rEQUIREDFLAG == "Y") { + String? val = dff.getContactDetailsList?.sEGMENTVALUEDSP; + String? varch = dff.getContactDetailsList?.vARCHAR2VALUE; + if ((val == null || val.trim().isEmpty) && (varch == null || varch.trim().isEmpty)) { + String name = dff.sEGMENTPROMPT ?? LocaleKeys.requiredField.tr(); + Utils.showToast(LocaleKeys.pleaseFillField.tr(args: [name])); + return; + } + } + } + setState(() { + _currentStep = 4; // Move to Step 4: Submit / Review + }); + } + + // --- Step 4 Submit Transaction to EBS --- + void _onSubmitTransaction() async { + Utils.showLoading(context); + + try { + List> values1 = _basicDetDffStructureList.map((e) { + String? dateVal; + String? varChar; + int? numberVal; + + if (e.fORMATTYPE == 'N') { + dateVal = null; + varChar = null; + numberVal = int.tryParse(e.getContactDetailsList?.sEGMENTVALUEDSP ?? "0"); + } else if (e.fORMATTYPE == 'X') { + dateVal = e.getContactDetailsList?.sEGMENTVALUEDSP?.toString(); + varChar = null; + numberVal = null; + } else if (e.eSERVICESVS != null && e.eSERVICESVS!.isNotEmpty) { + dateVal = null; + varChar = e.getContactDetailsList?.vARCHAR2VALUE; + numberVal = null; + } else { + dateVal = null; + varChar = e.getContactDetailsList?.sEGMENTVALUEDSP; + numberVal = null; + } + return ValidateEitTransactionModel( + dATEVALUE: dateVal, + nAME: e.aPPLICATIONCOLUMNNAME, + nUMBERVALUE: numberVal, + tRANSACTIONNUMBER: 1, + vARCHAR2VALUE: varChar, + ).toJson(); + }).toList(); + + List> values2 = _contactDffStructureList.map((e) { + String? dateVal; + String? varChar; + int? numberVal; + + if (e.fORMATTYPE == 'N') { + dateVal = null; + varChar = null; + numberVal = int.tryParse(e.getContactDetailsList?.sEGMENTVALUEDSP ?? "0"); + } else if (e.fORMATTYPE == 'X') { + dateVal = e.getContactDetailsList?.dATEVALUE ?? e.getContactDetailsList?.sEGMENTVALUEDSP; + varChar = null; + numberVal = null; + } else if (e.eSERVICESVS != null && e.eSERVICESVS!.isNotEmpty) { + dateVal = null; + varChar = e.getContactDetailsList?.vARCHAR2VALUE; + numberVal = null; + } else { + dateVal = null; + numberVal = null; + varChar = e.getContactDetailsList?.sEGMENTVALUEDSP; + } + return ValidateEitTransactionModel( + dATEVALUE: dateVal, + nAME: e.aPPLICATIONCOLUMNNAME, + nUMBERVALUE: numberVal, + tRANSACTIONNUMBER: 1, + vARCHAR2VALUE: varChar, + ).toJson(); + }).toList(); + + List> values3 = _contactColsStructureList.map((e) { + String? dateVal; + String? varChar1; + int? numberVal; + + if (e.dATATYPE == 'VARCHAR2') { + dateVal = null; + numberVal = null; + if (e.objectValuesList != null && e.objectValuesList!.isNotEmpty) { + varChar1 = e.getContactDetailsList?.vARCHAR2VALUE ?? ""; + } else { + varChar1 = e.getContactDetailsList?.sEGMENTVALUEDSP ?? ""; + } + } else if (e.dATATYPE == 'DATE') { + dateVal = e.getContactDetailsList?.sEGMENTVALUEDSP?.toString(); + varChar1 = null; + numberVal = null; + } else if (e.dATATYPE == 'NUMBER') { + dateVal = null; + varChar1 = null; + numberVal = int.tryParse(e.getContactDetailsList?.sEGMENTVALUEDSP ?? "0"); + } + + // Format National ID for Oracle EBS (X-XXXX-XXXX-X) e.g. 2-5446-0227-5 + String colNameUpper = (e.aPPLICATIONCOLUMNNAME ?? '').toUpperCase().trim(); + if (colNameUpper.contains('NATIONAL_IDENTIFIER') || + colNameUpper.contains('CIVIL_ID') || + colNameUpper.contains('IQAMA')) { + if (varChar1 != null && varChar1.isNotEmpty) { + String clean = varChar1.replaceAll('-', '').trim(); + if (clean.length == 10) { + varChar1 = "${clean.substring(0, 1)}-${clean.substring(1, 5)}-${clean.substring(5, 9)}-${clean.substring(9, 10)}"; + } + } + } + + return ValidateEitTransactionModel( + dATEVALUE: dateVal, + nAME: e.aPPLICATIONCOLUMNNAME, + nUMBERVALUE: numberVal, + tRANSACTIONNUMBER: 1, + vARCHAR2VALUE: varChar1, + ).toJson(); + }).toList(); + + List> valuesFinal = [...values1, ...values2, ...values3]; + + SubmitContactTransactionList? submitResult = await ProfileApiClient().submitContactTransactionAddAndUpdate("ADD", null, valuesFinal); + + Utils.hideLoading(context); + + if (submitResult != null && submitResult.pTRANSACTIONID != null) { + await Navigator.pushNamed( + context, + AppRoutes.requestSubmitScreen, + arguments: RequestSubmitScreenParams( + LocaleKeys.profile_familyDetails.tr(), + submitResult.pTRANSACTIONID!, + submitResult.pITEMKEY ?? '', + 'family_member', + ), + ); + + if (!AppState().cancelRequestTrancsection) { + Navigator.pop(context); + return; + } + + Utils.showLoading(context); + await LeaveBalanceApiClient().cancelHrTransaction(submitResult.pTRANSACTIONID!); + Utils.hideLoading(context); + Navigator.pop(context); + } else { + Utils.showToast(LocaleKeys.dependentAddedSuccessfully.tr(), isSuccess: true); + Navigator.pop(context); + } + } catch (ex) { + Utils.hideLoading(context); + Utils.handleException(ex, context, null); + } + } + + @override + Widget build(BuildContext context) { + bool isAr = AppState().isArabic(context); + + return Scaffold( + backgroundColor: Colors.white, + appBar: AppBarWidget( + context, + title: LocaleKeys.addDependent.tr(), + showHomeButton: true, + onBackTapped: () { + if (_currentStep > 1) { + setState(() { + _currentStep--; + }); + } else { + Navigator.maybePop(context); + } + }, + onHomeTapped: () { + Navigator.pushNamedAndRemoveUntil( + context, + ApiConsts.ISDependetLogin ? AppRoutes.mazayaDiscover : AppRoutes.dashboard, + (Route route) => false, + ); + }, + ), + body: SafeArea( + child: Column( + children: [ + // Stepper: 1 Verify -> 2 Basic -> 3 Additional -> 4 Submit + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), + child: _buildStepper(isAr), + ), + + // Step Content + Expanded( + child: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8), + child: _buildCurrentStepContent(isAr), + ), + ), + + // Pinned Bottom Button(s) - Reusing DefaultButton with gradient + _buildBottomButtons(isAr), + ], + ), + ), + ); + } + + // --- Stepper Widget --- + Widget _buildStepper(bool isAr) { + List> steps = >[ + {'num': '1', 'title': LocaleKeys.stepVerify.tr()}, + {'num': '2', 'title': LocaleKeys.stepBasic.tr()}, + {'num': '3', 'title': LocaleKeys.stepAdditional.tr()}, + {'num': '4', 'title': LocaleKeys.submit.tr()}, + ]; + + Color completedColor = const Color(0xFF199F6A); // completed colors as per figma + Color activeColor = const Color(0xFFFF8A00); + Color inactiveBorderColor = const Color(0xFFE2E8F0); + Color inactiveTextColor = const Color(0xFF64748B); + + const double circleSize = 32.0; + + return LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + double availableWidth = constraints.maxWidth; + // The stepper is left-aligned and spans ~75-80% of width on mobile + double stepSpacing = ((availableWidth * 0.78 - circleSize) / (steps.length - 1)).clamp(68.0, 84.0); + + return Align( + alignment: AlignmentDirectional.centerStart, + child: Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + for (int i = 0; i < steps.length; i++) ...[ + if (i < steps.length - 1) + SizedBox( + width: stepSpacing, + child: Stack( + clipBehavior: Clip.none, + children: [ + // Connecting line from right edge of circle to next circle + Positioned( + top: (circleSize - 2) / 2, + left: isAr ? null : circleSize, + right: isAr ? circleSize : null, + width: stepSpacing - circleSize, + child: Container( + height: 2, + color: (i + 1) < _currentStep ? completedColor : inactiveBorderColor, + ), + ), + // Circle + Title (left-aligned with circle) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + _buildStepCircle( + i: i, + steps: steps, + completedColor: completedColor, + activeColor: activeColor, + inactiveBorderColor: inactiveBorderColor, + inactiveTextColor: inactiveTextColor, + circleSize: circleSize, + ), + const SizedBox(height: 6), + steps[i]['title']!.toText12( + isBold: (i + 1) == _currentStep, + color: (i + 1) < _currentStep + ? completedColor + : ((i + 1) == _currentStep ? activeColor : inactiveTextColor), + ), + ], + ), + ], + ), + ) + else + // Last step (Submit) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + _buildStepCircle( + i: i, + steps: steps, + completedColor: completedColor, + activeColor: activeColor, + inactiveBorderColor: inactiveBorderColor, + inactiveTextColor: inactiveTextColor, + circleSize: circleSize, + ), + const SizedBox(height: 6), + steps[i]['title']!.toText12( + isBold: (i + 1) == _currentStep, + color: (i + 1) < _currentStep + ? completedColor + : ((i + 1) == _currentStep ? activeColor : inactiveTextColor), + ), + ], + ), + ], + ], + ), + ); + }, + ); + } + + Widget _buildStepCircle({ + required int i, + required List> steps, + required Color completedColor, + required Color activeColor, + required Color inactiveBorderColor, + required Color inactiveTextColor, + required double circleSize, + }) { + bool isCompleted = (i + 1) < _currentStep; + bool isActive = (i + 1) == _currentStep; + + return Container( + width: circleSize, + height: circleSize, + decoration: BoxDecoration( + color: isCompleted + ? completedColor + : (isActive ? activeColor : Colors.white), + shape: BoxShape.circle, + border: Border.all( + color: isCompleted + ? completedColor + : (isActive ? activeColor : inactiveBorderColor), + width: 1.5, + ), + ), + alignment: Alignment.center, + child: steps[i]['num']!.toText13( + isBold: true, + color: (isCompleted || isActive) ? Colors.white : inactiveTextColor, + ), + ); + } + + // --- Step Content Switcher --- + Widget _buildCurrentStepContent(bool isAr) { + switch (_currentStep) { + case 1: + return _buildStep1Verify(isAr); + case 2: + return _buildStep2BasicInfo(isAr); + case 3: + return _buildStep3AdditionalInfo(isAr); + case 4: + return _buildStep4Submit(isAr); + default: + return _buildStep1Verify(isAr); + } + } + + // --- STEP 1: VERIFY --- + Widget _buildStep1Verify(bool isAr) { + String dobDisplay = _selectedDob != null + ? DateFormat('dd / MM / yyyy').format(_selectedDob!) + : LocaleKeys.dateFormatPlaceholder.tr(); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 12), + LocaleKeys.findYourDependent.tr().toText20( + isBold: true, + color: const Color(0xFF1E293B), + ), + const SizedBox(height: 8), + (_isLegislationSA + ? LocaleKeys.findYourDependentDesc.tr() + : LocaleKeys.enterDependentDetailsToContinue.tr()) + .toText14( + color: const Color(0xFF64748B), + ), + const SizedBox(height: 24), + + // National ID + _buildFigmaTextField( + "${LocaleKeys.nationalId.tr()}*", + _isLegislationSA + ? LocaleKeys.enter10DigitNationalId.tr() + : LocaleKeys.enterFieldPlaceholder.tr(args: [LocaleKeys.nationalId.tr()]), + controller: _nationalIdController, + suffixIconData: Icons.badge_outlined, + isInputTypeNum: _isLegislationSA, + maxLength: _isLegislationSA ? 10 : 25, + backgroundColor: Colors.white, + borderColor: const Color(0xFFE6E9E8), + labelColor: const Color(0xff2B353E), + textColor: const Color(0xff2B353E), + suffixIconColor: const Color(0xFF64748B), + inputFormatters: _isLegislationSA + ? [ + FilteringTextInputFormatter.digitsOnly, + LengthLimitingTextInputFormatter(10), + ] + : [ + LengthLimitingTextInputFormatter(25), + ], + ), + const SizedBox(height: 16), + + // Date of Birth + _buildFigmaTextField( + "${LocaleKeys.dateOfBirth.tr()}*", + dobDisplay, + isReadOnly: true, + backgroundColor: Colors.white, + borderColor: const Color(0xFFE6E9E8), + labelColor: const Color(0xff2B353E), + textColor: _selectedDob != null ? const Color(0xff2B353E) : const Color(0xFF94A3B8), + suffixIconColor: const Color(0xFF64748B), + suffixIconData: Icons.calendar_today_outlined, + onTap: () => _pickDateOfBirth(context), + ), + const SizedBox(height: 24), + ], + ); + } + + // --- STEP 2: BASIC INFO (DYNAMICALLY GENERATED FROM CONTACT COLS & BASIC DET DFF) --- + Widget _buildStep2BasicInfo(bool isAr) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 12), + LocaleKeys.basicInfo.tr().toText20( + isBold: true, + color: const Color(0xFF1E293B), + ), + const SizedBox(height: 6), + (_isLegislationSA ? LocaleKeys.basicInfoDesc.tr() : LocaleKeys.enterBasicDetailsDesc.tr()).toText14( + color: const Color(0xFF64748B), + ), + const SizedBox(height: 20), + + // Dynamic fields from Contact Cols Structure + for (int i = 0; i < _contactColsStructureList.length; i++) + _buildDynamicColField(_contactColsStructureList[i], i), + + // Dynamic fields from Basic Det DFF Structure + for (int i = 0; i < _basicDetDffStructureList.length; i++) + _buildDynamicBasicDffField(_basicDetDffStructureList[i], i), + + const SizedBox(height: 12), + ], + ); + } + + // --- STEP 3: ADDITIONAL INFO (DYNAMICALLY GENERATED FROM CONTACT DFF) --- + Widget _buildStep3AdditionalInfo(bool isAr) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 12), + LocaleKeys.additionalInfo.tr().toText20( + isBold: true, + color: const Color(0xFF1E293B), + ), + const SizedBox(height: 6), + LocaleKeys.additionalInfoDesc.tr().toText14( + color: const Color(0xFF64748B), + ), + const SizedBox(height: 20), + + // Dynamic fields from Contact DFF Structure + for (int i = 0; i < _contactDffStructureList.length; i++) + _buildDynamicContactDffField(_contactDffStructureList[i], i), + + const SizedBox(height: 12), + ], + ); + } + + // Helper to extract field value dynamically for Review summary + String _findFieldValue(List keywords, [List? excludeKeywords]) { + for (var item in _contactColsStructureList) { + String colName = (item.aPPLICATIONCOLUMNNAME ?? '').toUpperCase(); + String prompt = (item.sEGMENTPROMPT ?? '').toUpperCase(); + if (excludeKeywords != null && excludeKeywords.any((ex) => colName.contains(ex.toUpperCase()) || prompt.contains(ex.toUpperCase()))) { + continue; + } + for (var k in keywords) { + String key = k.toUpperCase(); + if (colName.contains(key) || prompt.contains(key)) { + String? val = item.getContactDetailsList?.sEGMENTVALUEDSP; + if (val != null && val.trim().isNotEmpty) return val.trim(); + String? v2 = item.getContactDetailsList?.vARCHAR2VALUE; + if (v2 != null && v2.trim().isNotEmpty) return v2.trim(); + } + } + } + for (var item in _basicDetDffStructureList) { + String colName = (item.aPPLICATIONCOLUMNNAME ?? '').toUpperCase(); + String prompt = (item.sEGMENTPROMPT ?? '').toUpperCase(); + if (excludeKeywords != null && excludeKeywords.any((ex) => colName.contains(ex.toUpperCase()) || prompt.contains(ex.toUpperCase()))) { + continue; + } + for (var k in keywords) { + String key = k.toUpperCase(); + if (colName.contains(key) || prompt.contains(key)) { + String? val = item.getContactDetailsList?.sEGMENTVALUEDSP; + if (val != null && val.trim().isNotEmpty) return val.trim(); + String? v2 = item.getContactDetailsList?.vARCHAR2VALUE; + if (v2 != null && v2.trim().isNotEmpty) return v2.trim(); + } + } + } + for (var item in _contactDffStructureList) { + String colName = (item.aPPLICATIONCOLUMNNAME ?? '').toUpperCase(); + String prompt = (item.sEGMENTPROMPT ?? '').toUpperCase(); + if (excludeKeywords != null && excludeKeywords.any((ex) => colName.contains(ex.toUpperCase()) || prompt.contains(ex.toUpperCase()))) { + continue; + } + for (var k in keywords) { + String key = k.toUpperCase(); + if (colName.contains(key) || prompt.contains(key)) { + String? val = item.getContactDetailsList?.sEGMENTVALUEDSP; + if (val != null && val.trim().isNotEmpty) return val.trim(); + String? v2 = item.getContactDetailsList?.vARCHAR2VALUE; + if (v2 != null && v2.trim().isNotEmpty) return v2.trim(); + } + } + } + return ''; + } + + // --- STEP 4: SUBMIT / REVIEW --- + Widget _buildStep4Submit(bool isAr) { + String natId = _findFieldValue(['NATIONAL_IDENTIFIER', 'CIVIL_ID', 'IQAMA']); + if (natId.isEmpty) natId = _nationalIdController.text; + + String fn = _findFieldValue(['FIRST_NAME']); + String ln = _findFieldValue(['LAST_NAME', 'FAMILY_NAME']); + String fullName = "$fn $ln".trim(); + if (fullName.isEmpty) { + fullName = _findFieldValue(['CONTACT_NAME', 'FULL_NAME']); + } + if (fullName.isEmpty && _patientInfo != null) { + fullName = isAr ? _patientInfo!.fullNameAr : _patientInfo!.fullNameEn; + } + + String arFn = _findFieldValue(['ARABIC_FIRST', 'FIRST_NAME_AR']); + String arLn = _findFieldValue(['ARABIC_FAMILY', 'LAST_NAME_AR', 'ARABIC_LAST']); + String arFullName = "$arFn $arLn".trim(); + if (arFullName.isEmpty && _patientInfo != null) { + arFullName = _patientInfo!.fullNameAr; + } + + String dobDisplay = _findFieldValue(['DATE_OF_BIRTH', 'DOB']); + if (dobDisplay.isEmpty && _selectedDob != null) { + dobDisplay = DateFormat('dd / MM / yyyy').format(_selectedDob!); + } + + String rel = _findFieldValue(['CONTACT_TYPE', 'RELATIONSHIP']); + String sponsor = _findFieldValue(['SPONSOR'], ['DETAIL', 'تفاصيل']); + String sponsorDetail = _findFieldValue(['SPONSOR_DETAIL', 'DEP_SPONSOR', 'تفاصيل كفيل', 'DETAIL']); + String insurance = _findFieldValue(['INSURANCE', 'MED']); + String startDate = _findFieldValue(['START', 'DATE_START']); + if (startDate.isEmpty) startDate = dobDisplay; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 12), + LocaleKeys.reviewAndSubmit.tr().toText20( + isBold: true, + color: const Color(0xFF1E293B), + ), + const SizedBox(height: 6), + LocaleKeys.reviewAndSubmitDesc.tr().toText14( + color: const Color(0xFF64748B), + ), + const SizedBox(height: 20), + + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: const Color(0xFFE2E8F0)), + boxShadow: const [ + BoxShadow( + color: Color(0x08000000), + blurRadius: 8, + offset: Offset(0, 2), + ), + ], + ), + child: Column( + children: [ + _buildReviewRow(LocaleKeys.nationalId.tr(), natId), + const Divider(height: 20), + _buildReviewRow(LocaleKeys.fullName.tr(), fullName), + if (arFullName.isNotEmpty) ...[ + const Divider(height: 20), + _buildReviewRow(LocaleKeys.arabicName.tr(), arFullName), + ], + const Divider(height: 20), + _buildReviewRow(LocaleKeys.dateOfBirth.tr(), dobDisplay), + if (rel.isNotEmpty) ...[ + const Divider(height: 20), + _buildReviewRow(LocaleKeys.relationship.tr(), rel), + ], + if (sponsor.isNotEmpty) ...[ + const Divider(height: 20), + _buildReviewRow(LocaleKeys.sponsor.tr(), sponsor), + ], + if (sponsorDetail.isNotEmpty) ...[ + const Divider(height: 20), + _buildReviewRow(LocaleKeys.dependentSponsorDetail.tr(), sponsorDetail), + ], + if (insurance.isNotEmpty) ...[ + const Divider(height: 20), + _buildReviewRow(LocaleKeys.insuranceEligibility.tr(), insurance), + ], + if (startDate.isNotEmpty) ...[ + const Divider(height: 20), + _buildReviewRow(LocaleKeys.relationshipStartDate.tr(), startDate), + ], + ], + ), + ), + const SizedBox(height: 24), + ], + ); + } + + Widget _buildReviewRow(String title, String value) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + flex: 4, + child: title.toText13( + color: const Color(0xFF64748B), + ), + ), + Expanded( + flex: 6, + child: (value.isNotEmpty ? value : "-").toText14( + isBold: true, + textAlign: TextAlign.end, + color: const Color(0xFF1E293B), + ), + ), + ], + ); + } + + // --- Bottom Buttons (Always reusing DefaultButton with app gradient!) --- + Widget _buildBottomButtons(bool isAr) { + if (_currentStep == 1) { + bool canProceed = _isStep1Valid() && !_isLoading; + return DefaultButton( + _isLegislationSA ? LocaleKeys.searchUser.tr() : LocaleKeys.next.tr(), + canProceed ? _onSearchUser : null, + iconData: _isLegislationSA ? Icons.search : null, + isTextExpanded: false, + height: 48, + borderRadius: 12, + textColor: canProceed ? Colors.white : const Color(0xFF8E95A2), + ).insideContainer; + } + + VoidCallback onBack = () { + setState(() { + _currentStep--; + }); + }; + + bool canProceed = false; + VoidCallback onNext; + String nextLabel; + + if (_currentStep == 2) { + canProceed = _isStep2Valid(); + onNext = _onNextFromBasicInfo; + nextLabel = LocaleKeys.next.tr(); + } else if (_currentStep == 3) { + canProceed = _isStep3Valid(); + onNext = _onNextFromAdditionalInfo; + nextLabel = LocaleKeys.next.tr(); + } else { + canProceed = true; + onNext = _onSubmitTransaction; + nextLabel = LocaleKeys.submitRequest.tr(); + } + + return Container( + color: Colors.white, + padding: const EdgeInsets.only(top: 16, bottom: 16, right: 20, left: 20), + child: Row( + children: [ + // Back Button: reuses DefaultButton with outline styling + Expanded( + child: DefaultButton( + LocaleKeys.back.tr(), + onBack, + colors: const [Colors.white, Colors.white], + textColor: MyColors.gradiantEndColor, + border: Border.all(color: MyColors.gradiantEndColor, width: 1.5), + height: 48, + borderRadius: 12, + ), + ), + const SizedBox(width: 16), + // Next / Submit Button: reuses DefaultButton with canonical gradient + Expanded( + child: DefaultButton( + nextLabel, + canProceed ? onNext : null, + height: 48, + borderRadius: 12, + textColor: canProceed ? Colors.white : const Color(0xFF8E95A2), + ), + ), + ], + ), + ); + } +} diff --git a/lib/ui/profile/dependent_details_screen.dart b/lib/ui/profile/dependent_details_screen.dart new file mode 100644 index 0000000..8b7d150 --- /dev/null +++ b/lib/ui/profile/dependent_details_screen.dart @@ -0,0 +1,861 @@ +import 'dart:io'; + +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:mohem_flutter_app/api/profile_api_client.dart'; +import 'package:mohem_flutter_app/app_state/app_state.dart'; +import 'package:mohem_flutter_app/classes/utils.dart'; +import 'package:mohem_flutter_app/classes/consts.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/get_employee_contacts.model.dart'; +import 'package:mohem_flutter_app/ui/profile/mazaya_access_result_screen.dart'; +import 'package:mohem_flutter_app/widgets/bottom_sheets/confirm_bottom_sheet.dart'; +import 'package:mohem_flutter_app/widgets/button/default_button.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:share_plus/share_plus.dart'; + +class _ShieldIconPainter extends CustomPainter { + final Color color; + final bool isGranted; + + _ShieldIconPainter({required this.color, required this.isGranted}); + + @override + void paint(Canvas canvas, Size size) { + Paint paint = + Paint() + ..color = color + ..style = PaintingStyle.stroke + ..strokeWidth = 2.0 + ..strokeCap = StrokeCap.round + ..strokeJoin = StrokeJoin.round; + + double w = size.width; + double h = size.height; + + Path path = Path(); + path.moveTo(w * 0.5, h * 0.1); + path.cubicTo(w * 0.72, h * 0.1, w * 0.88, h * 0.18, w * 0.88, h * 0.38); + path.cubicTo(w * 0.88, h * 0.65, w * 0.68, h * 0.84, w * 0.5, h * 0.92); + path.cubicTo(w * 0.32, h * 0.84, w * 0.12, h * 0.65, w * 0.12, h * 0.38); + path.cubicTo(w * 0.12, h * 0.18, w * 0.28, h * 0.1, w * 0.5, h * 0.1); + path.close(); + + canvas.drawPath(path, paint); + + if (isGranted) { + Path checkPath = Path(); + checkPath.moveTo(w * 0.34, h * 0.49); + checkPath.lineTo(w * 0.46, h * 0.61); + checkPath.lineTo(w * 0.66, h * 0.39); + canvas.drawPath(checkPath, paint); + } else { + canvas.drawLine( + Offset(w * 0.38, h * 0.38), + Offset(w * 0.62, h * 0.62), + paint, + ); + canvas.drawLine( + Offset(w * 0.62, h * 0.38), + Offset(w * 0.38, h * 0.62), + paint, + ); + } + } + + @override + bool shouldRepaint(covariant _ShieldIconPainter oldDelegate) => + color != oldDelegate.color || isGranted != oldDelegate.isGranted; +} + +class DependentDetailsScreen extends StatefulWidget { + final GetEmployeeContactsList? contact; + + // Null = just viewing (arrived by tapping the card, no top badge). True/false = this screen + // is being opened right after a successful grant/revoke performed elsewhere (e.g. the + // dependents list's inline "Allow/Revoke Mazaya Access" link), so the matching badge shows + // on first build. Toggling from within this screen itself (_performToggleAccess) sets the + // equivalent local state directly instead of needing this. + final bool? recentAction; + + const DependentDetailsScreen({Key? key, this.contact, this.recentAction}) + : super(key: key); + + @override + _DependentDetailsScreenState createState() => _DependentDetailsScreenState(); +} + +class _DependentDetailsScreenState extends State { + GetEmployeeContactsList? _contact; + bool _argsLoaded = false; + + // null = just viewing (arrived by tapping the card, no badge) - true = just granted here, + // false = just revoked here. Set once per toggle in _performToggleAccess; a tap-only visit + // never sets it, so the top badge only ever reflects an action just taken on this screen. + bool? _recentAction; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + if (!_argsLoaded) { + if (widget.contact != null) { + _contact = widget.contact; + } else { + Object? args = ModalRoute.of(context)?.settings.arguments; + if (args is GetEmployeeContactsList) { + _contact = args; + } else if (args is Map && args['contact'] is GetEmployeeContactsList) { + _contact = args['contact'] as GetEmployeeContactsList; + } + } + _recentAction = widget.recentAction; + _argsLoaded = true; + } + } + + bool get _isAccessAllowed { + if (_contact == null) return false; + String? flag = + _contact!.mAZAYAACCESSFLG ?? _contact!.mazayaAccess?.toString(); + return flag?.trim().toUpperCase() == 'Y'; + } + + bool get _isNhicVerified { + if (_contact == null) return false; + return _contact!.pNHICVERIFIEDFLAG?.trim().toUpperCase() == 'Y'; + } + + String _getInitials(String? name) { + if (name == null || name.trim().isEmpty) return "D"; + List parts = + name + .trim() + .split(RegExp(r'\s+')) + .where((String p) => p.isNotEmpty) + .toList(); + if (parts.isEmpty) return "D"; + if (parts.length == 1) { + return parts.first.length >= 2 + ? parts.first.substring(0, 2).toUpperCase() + : parts.first.toUpperCase(); + } + return "${parts.first[0]}${parts.last[0]}".toUpperCase(); + } + + String _getFirstName(String? name) { + if (name == null || name.trim().isEmpty) return ""; + List parts = + name + .trim() + .split(RegExp(r'\s+')) + .where((String p) => p.isNotEmpty) + .toList(); + if (parts.isEmpty) return ""; + return parts.first; + } + + DateTime? _parseBirthDate(String? dateStr) { + if (dateStr == null || dateStr.trim().isEmpty) return null; + String s = dateStr.trim(); + + 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 (_) {} + } + + 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 (_) {} + } + + DateTime? dt = DateTime.tryParse(s); + if (dt != null) return DateTime(dt.year, dt.month, dt.day); + + // DATE_OF_BIRTH comes back as "M/d/yyyy h:mm:ss a" (e.g. "10/25/1997 12:00:00 AM") - strip + // that clock-time suffix first, since none of the date-only patterns below can match it + // with it still attached (parseStrict requires the whole string to match the pattern). + String dateOnly = + s + .replaceFirst( + RegExp( + r'\s+\d{1,2}:\d{2}(:\d{2})?\s*(AM|PM)?$', + caseSensitive: false, + ), + '', + ) + .trim(); + + // MM/dd/yyyy before dd/MM/yyyy - the real source is confirmed month-first, so for an + // ambiguous case like "5/6/1990" this must resolve to May 6, not be misread as 5 June. + List formats = [ + 'yyyy-MM-dd', + 'MM/dd/yyyy', + 'dd/MM/yyyy', + 'dd-MM-yyyy', + 'yyyy/MM/dd', + 'dd-MMM-yyyy', + 'dd MMM yyyy', + ]; + for (String fmt in formats) { + try { + DateTime parsed = DateFormat(fmt, 'en_US').parseStrict(dateOnly); + return DateTime(parsed.year, parsed.month, parsed.day); + } catch (_) {} + } + + return null; + } + + String _formatDob(DateTime? dob, String? raw) { + if (dob != null) { + return DateFormat('dd-MM-yyyy', 'en_US').format(dob); + } + if (raw != null && raw.trim().isNotEmpty) { + return raw.split(' ').first.replaceAll('/', '-'); + } + return "-"; + } + + // Masks everything but the last 3 digits, e.g. "2561615457" -> "*******457" - used both for + // the on-screen Key Details display and the share message, so the full ID is never shown or + // sent in plain text anywhere. + String _formatNationalId(String? nationalId) { + if (nationalId == null || nationalId.trim().isEmpty) return "-"; + String clean = nationalId.replaceAll('-', '').trim(); + if (clean.length <= 3) return clean; + String lastDigits = clean.substring(clean.length - 3); + String mask = List.filled(clean.length - 3, '*').join(); + return '$mask$lastDigits'; + } + + void _onToggleAccess() { + if (_contact == null) return; + bool isAllowed = _isAccessAllowed; + String dependentName = _contact!.cONTACTNAME ?? ""; + + ConfirmBottomSheet.show( + context, + title: + isAllowed + ? LocaleKeys.revokeMazayaAccess.tr() + : LocaleKeys.allowMazayaAccess.tr(), + message: + isAllowed + ? LocaleKeys.confirmRevokeMazayaAccess.tr( + args: [dependentName], + ) + : LocaleKeys.confirmGrantMazayaAccess.tr( + args: [dependentName], + ), + icon: + isAllowed ? Icons.remove_circle_outline : Icons.check_circle_outline, + iconColor: isAllowed ? const Color(0xFFDC2626) : const Color(0xFF168A89), + confirmText: LocaleKeys.yes.tr(), + cancelText: LocaleKeys.no.tr(), + onConfirm: _performToggleAccess, + ); + } + + Future _performToggleAccess() async { + if (_contact == null) return; + bool isAllowed = _isAccessAllowed; + String action = isAllowed ? "Revoke" : "Grant"; + + try { + Utils.showLoading(context); + await ProfileApiClient().processContactMazayaAccess( + accessAction: action, + contactRelationshipId: _contact!.cONTACTRELATIONSHIPID, + contactPersonId: _contact!.cONTACTPERSONID, + personId: _contact!.pERSONID, + patientIdentificationId: _contact!.nATIONALIDENTIFIER, + // DATE_OF_BIRTH, not the separate P_BIRTH_DATE response field - that one stays null + // for every contact until NHIC verification happens, so it's never a usable source here. + patientBirthDate: _contact!.dATEOFBIRTH, + nhicVerifiedFlag: _contact!.pNHICVERIFIEDFLAG, + ); + + setState(() { + _contact!.mAZAYAACCESSFLG = action == "Grant" ? "Y" : "N"; + _contact!.mazayaAccess = _contact!.mAZAYAACCESSFLG; + // Top badge reflects whichever action was just taken on this screen, not just "was + // granted at some point" - a later revoke flips it so a stale grant badge never lingers. + _recentAction = action == "Grant"; + }); + + Utils.hideLoading(context); + + // Stay on this same screen either way - it already reflects the new access state via + // setState above (status badge, top banner, action button, and the share-link section + // all update in place), so tapping a dependent card and successfully toggling access + // both land on the exact same screen instead of a separate "success" screen. The top + // badge itself is the confirmation now, so no toast on top of it. + } catch (ex) { + Utils.hideLoading(context); + Utils.handleException(ex, context, null); + } + } + + // Full (unmasked) DOB for the share message - distinct from _formatDob/_formatNationalId + // above, which are for the masked on-screen Key Details display. + String _formatDobForShare() { + DateTime? dob = _parseBirthDate(_contact?.dATEOFBIRTH); + if (dob != null) return DateFormat('dd-MM-yyyy').format(dob); + String raw = _contact?.dATEOFBIRTH?.trim() ?? ""; + if (raw.isEmpty) return ""; + if (raw.contains('T')) return raw.split('T').first.replaceAll('/', '-'); + if (raw.contains(' ')) return raw.split(' ').first.replaceAll('/', '-'); + return raw.replaceAll('/', '-'); + } + + // Same message built for both "Share Download Link" and "Copy Link" - they must always + // carry identical content, so this is the single source of truth for it. + String _buildMazayaInviteMessage(String linkToShare) { + String? nationalId = _contact?.nATIONALIDENTIFIER?.trim(); + String nationalIdLine = + (nationalId == null || nationalId.isEmpty) + ? LocaleKeys.nationalIdOrIqamaFallback.tr() + : "${LocaleKeys.nationalId.tr()}: ${nationalId.replaceAll('-', '')}"; + return "$linkToShare\n\n${LocaleKeys.mazayaInviteInstructions.tr()}\n\n$nationalIdLine\n${LocaleKeys.dateOfBirth.tr()}: ${_formatDobForShare()}"; + } + + // Shares the invite text together with the language-appropriate steps PDF from assets. + // Falls back to sharing just the text if the PDF can't be loaded/copied for any reason. + // Text-only share (the invite message + link). Kept separate from the guide share below + // because some targets (e.g. WhatsApp) drop the accompanying `text` entirely once a file is + // attached to the same share - splitting them guarantees the message itself is always visible. + Future _shareMazayaInviteLink(bool isAr, String linkToShare) async { + String message = _buildMazayaInviteMessage(linkToShare); + try { + await SharePlus.instance.share(ShareParams(text: message)); + } catch (_) {} + } + + // File-only share of the bundled access guide PDF - no accompanying text, so every target + // (including WhatsApp) just attaches the file with nothing to drop. + Future _shareMazayaAccessGuide(bool isAr) async { + try { + ByteData bytes = await rootBundle.load( + MazayaAccessResultScreen.getShareGuidePdfAssetPath(isAr), + ); + Directory tempDir = await getTemporaryDirectory(); + String localFileName = + isAr ? "mazaya_access_guide_ar.pdf" : "mazaya_access_guide_en.pdf"; + File pdfFile = File('${tempDir.path}/$localFileName'); + await pdfFile.writeAsBytes( + bytes.buffer.asUint8List(bytes.offsetInBytes, bytes.lengthInBytes), + ); + + await SharePlus.instance.share(ShareParams(files: [XFile(pdfFile.path)])); + } catch (_) {} + } + + @override + Widget build(BuildContext context) { + DateTime? dob = _parseBirthDate(_contact?.dATEOFBIRTH); + String initials = _getInitials(_contact?.cONTACTNAME); + String fullName = _contact?.cONTACTNAME ?? ""; + String firstName = _getFirstName(_contact?.cONTACTNAME); + String relation = + (_contact?.rELATIONSHIP != null && + _contact!.rELATIONSHIP!.trim().isNotEmpty) + ? _contact!.rELATIONSHIP!.trim() + : (_contact?.cONTACTTYPE?.trim() ?? ""); + String formattedId = _formatNationalId(_contact?.nATIONALIDENTIFIER); + String formattedDob = _formatDob(dob, _contact?.dATEOFBIRTH); + bool isAllowed = _isAccessAllowed; + bool isAr = AppState().isArabic(context); + String linkToShare = MazayaAccessResultScreen.getDownloadLink(); + + return Scaffold( + backgroundColor: const Color(0xFFF8FAFC), + appBar: PreferredSize( + preferredSize: const Size.fromHeight(56), + child: SafeArea( + child: Container( + height: 56, + color: const Color(0xFFF8FAFC), + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + children: [ + InkWell( + borderRadius: BorderRadius.circular(20), + onTap: () => Navigator.pop(context), + child: const Padding( + padding: EdgeInsets.all(8.0), + child: Icon( + Icons.arrow_back_ios_new_rounded, + size: 20, + color: Color(0xFF1E293B), + ), + ), + ), + const SizedBox(width: 8), + LocaleKeys.dependentDetails.tr().toText20( + color: const Color(0xFF1E293B), + ), + const Spacer(), + InkWell( + borderRadius: BorderRadius.circular(20), + onTap: + () => Navigator.popUntil( + context, + ModalRoute.withName( + ApiConsts.isDependetLogin + ? AppRoutes.mazayaDiscover + : AppRoutes.dashboard, + ), + ), + child: const Padding( + padding: EdgeInsets.all(8.0), + child: Icon( + Icons.home_outlined, + size: 24, + color: Color(0xFF1E293B), + ), + ), + ), + ], + ), + ), + ), + ), + body: SafeArea( + child: Column( + children: [ + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.fromLTRB(20, 12, 20, 30), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Shown only right after a grant/revoke action taken on this screen - not + // when just viewing a dependent by tapping into this screen (_recentAction + // is null in that case). + if (_recentAction != null) ...[ + Center( + child: Container( + width: 72, + height: 72, + decoration: BoxDecoration( + color: + _recentAction! + ? const Color(0xFFE6F7F2) + : const Color(0xFFFEECEB), + shape: BoxShape.circle, + ), + child: Center( + child: CustomPaint( + size: const Size(36, 36), + painter: _ShieldIconPainter( + color: + _recentAction! + ? const Color(0xFF168A89) + : const Color(0xFFED1C24), + isGranted: _recentAction!, + ), + ), + ), + ), + ), + const SizedBox(height: 10), + Center( + child: (_recentAction! + ? LocaleKeys.mazayaAccessGranted.tr() + : LocaleKeys.mazayaAccessRevoked.tr()) + .toText16( + weight: FontWeight.w500, + color: + _recentAction! + ? const Color(0xFF168A89) + : const Color(0xFFED1C24), + ), + ), + const SizedBox(height: 16), + ], + + // Profile Card (Mint Green) + Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: const Color(0xFFE8F7F2), + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: const Color(0xFFBCE6D7), + width: 1.2, + ), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // Avatar circle with initials + Container( + width: 52, + height: 52, + decoration: const BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + ), + child: Center( + child: initials.toText17( + color: const Color(0xFF168A89), + ), + ), + ), + const SizedBox(width: 14), + + // Dependent Name & Relation + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + fullName.toText16( + maxlines: 2, + weight: FontWeight.w500, + color: const Color(0xFF1E293B), + ), + if (relation.isNotEmpty) ...[ + const SizedBox(height: 3), + relation.toText12( + maxLine: 1, + weight: FontWeight.w500, + color: const Color(0xFF168A89), + ), + ], + ], + ), + ), + + // Verified badge on right - only when NHIC has actually verified this contact + if (_isNhicVerified) ...[ + const SizedBox(width: 8), + const Icon( + Icons.verified_rounded, + color: Color(0xFF059669), + size: 24, + ), + ], + ], + ), + ), + + const SizedBox(height: 16), + + // Key details Card + Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: const Color(0xFFF1F5F9)), + boxShadow: const [ + BoxShadow( + color: Color(0x08000000), + blurRadius: 8, + offset: Offset(0, 2), + ), + ], + ), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + LocaleKeys.keyDetails.tr().toText16( + weight: FontWeight.w500, + color: const Color(0xFF1E293B), + ), + if (_isNhicVerified) + LocaleKeys.verified.tr().toText13( + weight: FontWeight.w500, + color: const Color(0xFF059669), + ), + ], + ), + const SizedBox(height: 14), + const Divider( + height: 1, + thickness: 1, + color: Color(0xFFF1F5F9), + ), + const SizedBox(height: 14), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + LocaleKeys.nationalId.tr().toText13( + weight: FontWeight.w400, + color: const Color(0xFF64748B), + ), + formattedId.toText13( + weight: FontWeight.w500, + color: const Color(0xFF1E293B), + ), + ], + ), + const SizedBox(height: 12), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + LocaleKeys.dateOfBirth.tr().toText13( + weight: FontWeight.w400, + color: const Color(0xFF64748B), + ), + formattedDob.toText13( + weight: FontWeight.w500, + color: const Color(0xFF1E293B), + ), + ], + ), + ], + ), + ), + + const SizedBox(height: 24), + + // Services & actions section header + LocaleKeys.servicesAndActions.tr().toText17( + weight: FontWeight.w500, + color: const Color(0xFF1E293B), + ), + const SizedBox(height: 4), + LocaleKeys.startDependentFlowsDesc.tr().toText13( + weight: FontWeight.w400, + color: const Color(0xFF64748B), + ), + + const SizedBox(height: 12), + + // Mazaya access Card + Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: const Color(0xFFF1F5F9)), + boxShadow: const [ + BoxShadow( + color: Color(0x08000000), + blurRadius: 8, + offset: Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header Row: Mazaya access + Badge + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + LocaleKeys.mazayaAccess.tr().toText16( + weight: FontWeight.w500, + color: const Color(0xFF1E293B), + ), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 3, + ), + decoration: BoxDecoration( + color: + isAllowed + ? const Color(0xFFECFDF5) + : const Color(0xFFFEE2E2), + borderRadius: BorderRadius.circular(12), + ), + child: (isAllowed + ? LocaleKeys.active.tr() + : LocaleKeys.accessOff.tr()) + .toText12( + weight: FontWeight.w500, + color: + isAllowed + ? const Color(0xFF059669) + : const Color(0xFFEF4444), + ), + ), + ], + ), + const SizedBox(height: 6), + LocaleKeys.allowDependentMazayaOffersDesc + .tr( + args: [ + firstName.isNotEmpty ? firstName : fullName, + ], + ) + .toText13( + weight: FontWeight.w400, + color: const Color(0xFF64748B), + ), + const SizedBox(height: 16), + const Divider( + height: 1, + thickness: 1, + color: Color(0xFFF1F5F9), + ), + const SizedBox(height: 14), + + // Login method row + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + LocaleKeys.loginMethod.tr().toText13( + weight: FontWeight.w400, + color: const Color(0xFF64748B), + ), + LocaleKeys.nationalIdAndDob.tr().toText13( + weight: FontWeight.w500, + color: const Color(0xFF1E293B), + ), + ], + ), + const SizedBox(height: 12), + + // Verification row + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + LocaleKeys.verification.tr().toText13( + weight: FontWeight.w400, + color: const Color(0xFF64748B), + ), + LocaleKeys.sponsorOtp.tr().toText13( + weight: FontWeight.w500, + color: const Color(0xFF1E293B), + ), + ], + ), + + const SizedBox(height: 20), + + // Action Button: Allow Mazaya Access (Teal) or Revoke Mazaya Access (Red Outline) + InkWell( + borderRadius: BorderRadius.circular(12), + onTap: _onToggleAccess, + child: Container( + height: 48, + width: double.infinity, + decoration: BoxDecoration( + color: + isAllowed + ? const Color(0xFFFEF2F2) + : const Color(0xFF22B0A6), + borderRadius: BorderRadius.circular(12), + border: + isAllowed + ? Border.all( + color: const Color(0xFFFECACA), + width: 1.2, + ) + : null, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + width: 18, + height: 18, + child: CustomPaint( + painter: _ShieldIconPainter( + color: + isAllowed + ? const Color(0xFFDC2626) + : Colors.white, + isGranted: !isAllowed, + ), + ), + ), + const SizedBox(width: 8), + (isAllowed + ? LocaleKeys.revokeMazayaAccess.tr() + : LocaleKeys.allowMazayaAccess.tr()) + .toText16( + weight: FontWeight.w500, + color: + isAllowed + ? const Color(0xFFDC2626) + : Colors.white, + ), + ], + ), + ), + ), + + if (isAllowed) ...[ + const SizedBox(height: 12), + Center( + child: LocaleKeys.revokingPreventsAccessDesc + .tr() + .toText11( + weight: FontWeight.w400, + color: const Color(0xFF64748B), + isCenter: true, + ), + ), + ], + ], + ), + ), + ], + ), + ), + ), + // Two separate share actions (not one combined text+file share) - same bottom-sticky + // container convention used elsewhere in the app (e.g. "Join Marathon"), shown once + // access is granted. + if (isAllowed) + Row( + children: [ + // Secondary - same bg/border/text colors as the name+relation card at the top + // of this screen, for visual consistency within this page. + Expanded( + child: DefaultButton( + LocaleKeys.shareAccessGuide.tr(), + () => _shareMazayaAccessGuide(isAr), + colors: const [ + Color(0xFFE8F7F2), + Color(0xFFE8F7F2), + ], + border: Border.all( + color: const Color(0xFFBCE6D7), + width: 1.2, + ), + textColor: const Color(0xFF168A89), + ), + ), + const SizedBox(width: 12), + Expanded( + child: DefaultButton( + LocaleKeys.shareDownloadLink.tr(), + () => _shareMazayaInviteLink(isAr, linkToShare), + ), + ), + ], + ).insideContainer, + ], + ), + ), + ); + } +} diff --git a/lib/ui/profile/family_members.dart b/lib/ui/profile/family_members.dart index 7b33d47..f84f2cc 100644 --- a/lib/ui/profile/family_members.dart +++ b/lib/ui/profile/family_members.dart @@ -1,21 +1,24 @@ +import 'dart:developer'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.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/utils.dart'; import 'package:mohem_flutter_app/config/routes.dart'; -import 'package:mohem_flutter_app/extensions/int_extensions.dart'; import 'package:mohem_flutter_app/extensions/string_extensions.dart'; -import 'package:mohem_flutter_app/extensions/widget_extensions.dart'; import 'package:mohem_flutter_app/generated/locale_keys.g.dart'; import 'package:mohem_flutter_app/models/dashboard/menu_entries.dart'; import 'package:mohem_flutter_app/models/get_employee_contacts.model.dart'; import 'package:mohem_flutter_app/provider/dashboard_provider_model.dart'; +import 'package:mohem_flutter_app/ui/mazaya/widgets/mazaya_bottom_nav.dart'; import 'package:mohem_flutter_app/ui/profile/add_update_family_member.dart'; -import 'package:mohem_flutter_app/widgets/app_bar_widget.dart'; +import 'package:mohem_flutter_app/ui/profile/dependent_details_screen.dart'; +import 'package:mohem_flutter_app/widgets/bottom_sheets/confirm_bottom_sheet.dart'; import 'package:mohem_flutter_app/widgets/button/default_button.dart'; import 'package:provider/provider.dart'; +import 'package:shimmer/shimmer.dart'; class FamilyMembers extends StatefulWidget { const FamilyMembers({Key? key}) : super(key: key); @@ -25,9 +28,13 @@ class FamilyMembers extends StatefulWidget { } class _FamilyMembersState extends State { - List getEmployeeContactsList = []; + List getEmployeeContactsList = []; + bool fromMazaya = false; + bool _argsLoaded = false; + bool _isLoading = false; - int? relationId; + final TextEditingController _searchController = TextEditingController(); + String _searchQuery = ""; @override void initState() { @@ -35,18 +42,137 @@ class _FamilyMembersState extends State { getEmployeeContacts(); } - void getEmployeeContacts() async { + @override + void didChangeDependencies() { + super.didChangeDependencies(); + if (!_argsLoaded) { + Object? args = ModalRoute.of(context)?.settings.arguments; + if (args is Map && args['fromMazaya'] == true) { + fromMazaya = true; + } + _argsLoaded = true; + } + } + + @override + void dispose() { + _searchController.dispose(); + super.dispose(); + } + + bool _isAccessAllowed(GetEmployeeContactsList contact) { + String? flag = contact.mAZAYAACCESSFLG ?? contact.mazayaAccess?.toString(); + return flag?.trim().toUpperCase() == 'Y'; + } + + bool _isNhicVerified(GetEmployeeContactsList contact) { + return contact.pNHICVERIFIEDFLAG?.trim().toUpperCase() == 'Y'; + } + + void _confirmToggleMazayaAccess(GetEmployeeContactsList contact) { + bool isAllowed = _isAccessAllowed(contact); + String dependentName = contact.cONTACTNAME ?? ""; + + ConfirmBottomSheet.show( + context, + title: isAllowed ? LocaleKeys.revokeMazayaAccess.tr() : LocaleKeys.allowMazayaAccess.tr(), + message: isAllowed ? LocaleKeys.confirmRevokeMazayaAccess.tr(args: [dependentName]) : LocaleKeys.confirmGrantMazayaAccess.tr(args: [dependentName]), + icon: isAllowed ? Icons.remove_circle_outline : Icons.check_circle_outline, + iconColor: isAllowed ? const Color(0xFFDC2626) : const Color(0xFF168A89), + // Same gradient as the "Add another user" button on this screen (DefaultButton's default). + confirmButtonColors: const [MyColors.gradiantEndColor, MyColors.gradiantStartColor], + confirmText: LocaleKeys.yes.tr(), + cancelText: LocaleKeys.no.tr(), + onConfirm: () => _toggleMazayaAccess(contact), + ); + } + + Future _toggleMazayaAccess(GetEmployeeContactsList contact) async { + bool current = _isAccessAllowed(contact); + String action = current ? "Revoke" : "Grant"; + try { Utils.showLoading(context); - getEmployeeContactsList = await ProfileApiClient().getEmployeeContacts(); + await ProfileApiClient().processContactMazayaAccess( + accessAction: action, + contactRelationshipId: contact.cONTACTRELATIONSHIPID, + contactPersonId: contact.cONTACTPERSONID, + personId: contact.pERSONID, + patientIdentificationId: contact.nATIONALIDENTIFIER, + // DATE_OF_BIRTH, not the separate P_BIRTH_DATE response field - that one stays null + // for every contact until NHIC verification happens, so it's never a usable source here. + patientBirthDate: contact.dATEOFBIRTH, + nhicVerifiedFlag: contact.pNHICVERIFIEDFLAG, + ); + + // Keep this exact `contact` instance in sync too - it's what gets handed to + // DependentDetailsScreen below, and that's a different object than whatever the fresh + // list refetch returns, so without this it would show the old (pre-toggle) access state + // even though the screen's own "just granted/revoked" badge is correct. + contact.mAZAYAACCESSFLG = action == "Grant" ? "Y" : "N"; + contact.mazayaAccess = contact.mAZAYAACCESSFLG; + + List rawList = await ProfileApiClient().getEmployeeContacts(); + getEmployeeContactsList = rawList; + Utils.hideLoading(context); - setState(() {}); + + if (mounted) { + setState(() {}); + // Same destination as tapping the card itself, so toggling from either entry point + // lands on the exact same screen (grant/revoke badge, button, and share-link section + // all reflect the new state there) - no toast on top of it. + await Navigator.push(context, MaterialPageRoute(builder: (BuildContext cxt) => DependentDetailsScreen(contact: contact, recentAction: action == "Grant"))); + getEmployeeContacts(); + } } catch (ex) { + log("erros: ${ex.toString()}"); Utils.hideLoading(context); Utils.handleException(ex, context, null); } } + String _getInitials(String? name) { + if (name == null || name.trim().isEmpty) return "D"; + List parts = name.trim().split(RegExp(r'\s+')).where((String p) => p.isNotEmpty).toList(); + if (parts.isEmpty) return "D"; + if (parts.length == 1) { + return parts.first.length >= 2 ? parts.first.substring(0, 2).toUpperCase() : parts.first.toUpperCase(); + } + return "${parts.first[0]}${parts.last[0]}".toUpperCase(); + } + + List get _filteredList { + if (_searchQuery.trim().isEmpty) return getEmployeeContactsList; + String q = _searchQuery.trim().toLowerCase(); + return getEmployeeContactsList.where((GetEmployeeContactsList item) { + String name = (item.cONTACTNAME ?? "").toLowerCase(); + String rel = (item.rELATIONSHIP ?? "").toLowerCase(); + String personId = (item.pERSONID?.toString() ?? "").toLowerCase(); + String contactPersonId = (item.cONTACTPERSONID?.toString() ?? "").toLowerCase(); + String contactType = (item.cONTACTTYPE ?? "").toLowerCase(); + return name.contains(q) || rel.contains(q) || personId.contains(q) || contactPersonId.contains(q) || contactType.contains(q); + }).toList(); + } + + void getEmployeeContacts() async { + setState(() { + _isLoading = true; + }); + try { + List rawList = await ProfileApiClient().getEmployeeContacts(); + getEmployeeContactsList = rawList; + } catch (ex) { + Utils.handleException(ex, context, null); + } finally { + if (mounted) { + setState(() { + _isLoading = false; + }); + } + } + } + GetMenuEntriesList _getContactMenuEntries(BuildContext context) { List menuData = context.watch().getMenuEntriesList ?? []; List arr = menuData.where((GetMenuEntriesList e) => e.requestType == 'CONTACT').toList(); @@ -55,180 +181,390 @@ class _FamilyMembersState extends State { @override Widget build(BuildContext context) { + bool isAr = AppState().isArabic(context); GetMenuEntriesList menuEntries = _getContactMenuEntries(context); - bool canAddFamilyMember = menuEntries.addButton == 'Y'; + bool canAddFamilyMember = menuEntries.addButton == 'Y' || menuEntries.updateButton == 'Y'; bool canUpdateFamilyMember = menuEntries.updateButton == 'Y'; bool canDeleteFamilyMember = menuEntries.deleteButton == 'Y'; + List displayList = _filteredList; + return Scaffold( - appBar: AppBarWidget( - context, - title: LocaleKeys.profile_familyDetails.tr(), + backgroundColor: const Color(0xFFF8FAFC), + // Only shown when reached via the Mazaya "My Family" tab - this screen is also opened from + // the regular profile flow (fromMazaya == false), where the Mazaya bottom nav doesn't apply. + bottomNavigationBar: fromMazaya ? const MazayaBottomNav(currentTab: MazayaTab.myFamily) : null, + appBar: PreferredSize( + preferredSize: const Size.fromHeight(56), + child: SafeArea( + child: Container( + height: 56, + color: const Color(0xFFF8FAFC), + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + children: [ + !fromMazaya? InkWell( + borderRadius: BorderRadius.circular(20), + onTap: () => Navigator.pop(context), + child: const Padding(padding: EdgeInsets.all(8.0), child: Icon(Icons.arrow_back_ios_new_rounded, size: 20, color: Color(0xFF1E293B))), + ):SizedBox(), + const SizedBox(width: 8), + // Same title style as the other Mazaya tabs' headers (Discover/Saved use + // AppBarWidget's title.toText24(isBold: true, color: MyColors.darkTextColor)) - + // matched here since this screen builds its own header instead of AppBarWidget. + fromMazaya + ? LocaleKeys.myFamily.tr().toText24(isBold: true, color: MyColors.darkTextColor) + : LocaleKeys.profile_familyDetails.tr().toText20(isBold: true, color: const Color(0xFF1E293B)), + const Spacer(), + // Home button hidden in the Mazaya flow too, matching AppBarWidget's + // showHomeButton: false on the other tabs - MazayaBottomNav's own Discover tab + // already serves as "home" for this module. + if (!fromMazaya) + InkWell( + borderRadius: BorderRadius.circular(20), + onTap: () => Navigator.popUntil(context, ModalRoute.withName(AppRoutes.dashboard)), + child: const Padding(padding: EdgeInsets.all(8.0), child: Icon(Icons.home_outlined, size: 24, color: Color(0xFF1E293B))), + ), + ], + ), + ), ), - backgroundColor: MyColors.backgroundColor, - body: Column( - children: [ + ), + body: SafeArea( + child: Column( + children: [ + // Search Bar + Padding( + padding: const EdgeInsets.fromLTRB(20, 8, 20, 12), + child: Container( + height: 48, + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(12), border: Border.all(color: const Color(0xFFE2E8F0))), + padding: const EdgeInsets.symmetric(horizontal: 14), + child: Row( + children: [ + const Icon(Icons.search, color: Color(0xFF64748B), size: 20), + const SizedBox(width: 10), + Expanded( + child: TextField( + controller: _searchController, + onChanged: (String val) { + setState(() { + _searchQuery = val; + }); + }, + decoration: InputDecoration( + border: InputBorder.none, + isDense: true, + contentPadding: EdgeInsets.zero, + hintText: fromMazaya ? LocaleKeys.searchDependents.tr() : LocaleKeys.searchFamilyMembers.tr(), + hintStyle: const TextStyle(color: Color(0xFF94A3B8), fontSize: 14), + ), + ), + ), + if (_searchQuery.isNotEmpty) + InkWell( + onTap: () { + _searchController.clear(); + setState(() { + _searchQuery = ""; + }); + }, + child: const Padding(padding: EdgeInsets.symmetric(horizontal: 4), child: Icon(Icons.close, color: Color(0xFF94A3B8), size: 18)), + ), + ], + ), + ), + ), + + // Dependents list Expanded( - child: getEmployeeContactsList.isEmpty - ? Utils.getNoDataWidget(context) - : ListView.separated( - padding: const EdgeInsets.all(21), - separatorBuilder: (cxt, index) => 12.height, - itemCount: getEmployeeContactsList.length, - itemBuilder: (context, index) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - "${getEmployeeContactsList[index].cONTACTNAME}".toText16(color: MyColors.grey3AColor), - "${getEmployeeContactsList[index].rELATIONSHIP}".toText11(color: MyColors.textMixColor), - ]).paddingOnly(top: 15, left: 13, right: 13, bottom: 12), - 1.divider, - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - // todo @faitma clean below logic and write clean code. - RichText( - text: TextSpan( - children: [ - WidgetSpan( - child: Icon( - Icons.edit, - size: 14, - color: canUpdateFamilyMember ? MyColors.grey67Color : MyColors.lightGreyColor, - )), - TextSpan( - text: LocaleKeys.update.tr(), - style: TextStyle( - color: canUpdateFamilyMember ? MyColors.grey67Color : MyColors.lightGreyColor, fontSize: 12, letterSpacing: -0.36, fontWeight: FontWeight.w600), - recognizer: TapGestureRecognizer() - ..onTap = () async { - relationId = getEmployeeContactsList[index].cONTACTRELATIONSHIPID; - if (canUpdateFamilyMember && relationId != null) { - showUpdateAlertDialog(context, relationId, 2, "UPDATE"); - } - }) - ], - ), - ), - Container(height: 35, width: 1, color: const Color(0xffEFEFEF)), - RichText( - text: TextSpan( - children: [ - WidgetSpan( - child: Icon( - Icons.delete, - size: 15, - color: canDeleteFamilyMember ? MyColors.redColor : MyColors.lightGreyColor, - ), - ), - TextSpan( - text: LocaleKeys.remove.tr(), - style: TextStyle( - color: canDeleteFamilyMember ? MyColors.redColor : MyColors.lightGreyColor, fontSize: 12, letterSpacing: -0.36, fontWeight: FontWeight.w600), - ), - ], - ), - ).onPress(() { - relationId = getEmployeeContactsList[index].cONTACTRELATIONSHIPID; - if (canDeleteFamilyMember && relationId != null) { - showRemoveAlertDialog(context, relationId); - } - }), - ], - ), - ], - ).objectContainerView(disablePadding: true); - }), + child: + _isLoading + ? _buildDependentsShimmer() + : displayList.isEmpty + ? Utils.getNoDataWidget(context) + : ListView.separated( + padding: const EdgeInsets.fromLTRB(20, 4, 20, 20), + separatorBuilder: (BuildContext cxt, int index) => const SizedBox(height: 12), + itemCount: displayList.length, + itemBuilder: (BuildContext context, int index) { + return _buildDependentCard(displayList[index], isAr, canUpdateFamilyMember, canDeleteFamilyMember); + }, + ), ), + + // Bottom action button: + Add another user DefaultButton( - LocaleKeys.addNewFamilyMember.tr(), - canAddFamilyMember - ? () async { - Navigator.pushNamed( - context, - AppRoutes.addUpdateFamilyMember, - arguments: FamilyMemberScreenParams(null, flag: 1, actionType: "ADD"), - ); - } - : null) - .insideContainer, + fromMazaya ? LocaleKeys.addAnotherUser.tr() : LocaleKeys.addNewFamilyMember.tr(), + (canAddFamilyMember || fromMazaya) + ? () async { + await Navigator.pushNamed(context, AppRoutes.addDependentVerify, arguments: {'fromMazaya': fromMazaya}); + getEmployeeContacts(); + } + : null, + iconData: Icons.add, + isTextExpanded: false, + height: 48, + borderRadius: 12, + ).insideContainer, ], - )); + ), + ), + ); } - void showUpdateAlertDialog(BuildContext context, relationId, int flag, String actionType) { - Widget cancelButton = TextButton( - child: Text( - LocaleKeys.cancel.tr(), + // Same Shimmer.fromColors skeleton-card convention used elsewhere in the app (e.g. + // retailers_list_screen.dart's retailer/category shimmers), shaped like the + // real _buildDependentCard layout so the loading state doesn't jump around once it loads. + Widget _buildDependentsShimmer() { + return Shimmer.fromColors( + baseColor: const Color(0xFFE2E8F0), + highlightColor: const Color(0xFFF8FAFC), + child: ListView.separated( + padding: const EdgeInsets.fromLTRB(20, 4, 20, 20), + separatorBuilder: (BuildContext _, int __) => const SizedBox(height: 12), + itemCount: 6, + itemBuilder: (BuildContext _, int __) { + return Container( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 14), + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(16)), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container(width: 48, height: 48, decoration: const BoxDecoration(color: Colors.white, shape: BoxShape.circle)), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container(height: 18, width: 140, decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(4))), + const SizedBox(height: 6), + Container(height: 14, width: 90, decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(4))), + ], + ), + ), + ], + ), + const SizedBox(height: 14), + Container(height: 1, color: Colors.white), + const SizedBox(height: 12), + Center(child: Container(height: 14, width: 150, decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(4)))), + ], + ), + ); + }, ), - onPressed: () { - Navigator.pop(context); - }, ); - Widget continueButton = TextButton( - child: Text( - LocaleKeys.ok.tr(), - ), - onPressed: () { - Navigator.pop(context); - Navigator.pushNamed( - context, - AppRoutes.addUpdateFamilyMember, - arguments:FamilyMemberScreenParams(relationId, flag: 2, actionType: "UPDATE"), - ); + } + + Widget _buildDependentCard(GetEmployeeContactsList item, bool isAr, bool canUpdate, bool canDelete) { + bool isAllowed = _isAccessAllowed(item); + + return InkWell( + borderRadius: BorderRadius.circular(16), + onTap: () async { + await Navigator.push(context, MaterialPageRoute(builder: (BuildContext cxt) => DependentDetailsScreen(contact: item))); + getEmployeeContacts(); }, - ); - AlertDialog alert = AlertDialog( - title: Text( - LocaleKeys.confirm.tr(), + child: Container( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 14), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: const Color(0xFFF1F5F9)), + boxShadow: const [BoxShadow(color: Color(0x0A000000), blurRadius: 8, offset: Offset(0, 3))], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Top row: Avatar, Info, and Status Badge + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // Initials Circle + Container( + width: 48, + height: 48, + decoration: const BoxDecoration(color: Color(0xFFE6F5F3), shape: BoxShape.circle), + child: Center(child: Text(_getInitials(item.cONTACTNAME), style: const TextStyle(color: Color(0xFF168A89), fontWeight: FontWeight.w700, fontSize: 15))), + ), + const SizedBox(width: 12), + + // Name & Details + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Flexible( + child: Text( + item.cONTACTNAME ?? "", + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF1E293B)), + ), + ), + if (_isNhicVerified(item)) ...[const SizedBox(width: 8), const Icon(Icons.verified_rounded, color: Color(0xFF059669), size: 20)], + ], + ), + const SizedBox(height: 3), + Row( + children: [ + Flexible(child: (item.rELATIONSHIP ?? "").toText12(maxLine: 1, color: const Color(0xFF168A89))), + if (_isNhicVerified(item)) ...[ + " • ".toText12(color: const Color(0xFF94A3B8)), + LocaleKeys.verified.tr().toText12(weight: FontWeight.w500, color: const Color(0xFF059669)), + ], + ], + ), + ], + ), + ), + + // Trailing arrow (shown in Mazaya flow): the whole card opens + // DependentDetailsScreen on tap, so this just signals that. + // Same "arrow_next.svg" + RotatedBox-for-RTL convention used in menus_widget.dart + // and services_widget.dart on the dashboard. + if (fromMazaya) ...[ + const SizedBox(width: 8), + RotatedBox(quarterTurns: isAr ? 2 : 4, child: SvgPicture.asset("assets/images/arrow_next.svg", color: const Color(0xFF94A3B8), width: 14)), + ], + ], + ), + + const SizedBox(height: 14), + const Divider(height: 1, thickness: 1, color: Color(0xFFF1F5F9)), + const SizedBox(height: 12), + + // Bottom Action Row + if (fromMazaya) ...[ + // Mazaya Access Buttons: Revoke or Allow + Center( + child: InkWell( + borderRadius: BorderRadius.circular(8), + onTap: () => _confirmToggleMazayaAccess(item), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(isAllowed ? Icons.close : Icons.check, size: isAllowed ? 16 : 18, color: isAllowed ? const Color(0xFFDC2626) : const Color(0xFF168A89)), + const SizedBox(width: 6), + Text( + isAllowed ? LocaleKeys.revokeMazayaAccess.tr() : LocaleKeys.allowMazayaAccess.tr(), + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: isAllowed ? const Color(0xFFDC2626) : const Color(0xFF168A89), + decoration: TextDecoration.underline, + decorationColor: isAllowed ? const Color(0xFFDC2626) : const Color(0xFF168A89), + ), + ), + ], + ), + ), + ), + ), + ] else ...[ + // Profile Flow: Update and Remove action buttons + Row( + children: [ + Expanded( + child: InkWell( + borderRadius: BorderRadius.circular(10), + onTap: + canUpdate + ? () { + int? relationId = item.cONTACTRELATIONSHIPID; + if (relationId != null) { + showUpdateAlertDialog(context, relationId, 2, "UPDATE"); + } + } + : null, + child: Container( + height: 38, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(10), + border: Border.all(color: canUpdate ? const Color(0xFFCBD5E1) : const Color(0xFFE2E8F0)), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.edit_outlined, size: 15, color: canUpdate ? const Color(0xFF475569) : const Color(0xFF94A3B8)), + const SizedBox(width: 6), + LocaleKeys.update.tr().toText12(color: canUpdate ? const Color(0xFF475569) : const Color(0xFF94A3B8)), + ], + ), + ), + ), + ), + const SizedBox(width: 10), + Expanded( + child: InkWell( + borderRadius: BorderRadius.circular(10), + onTap: + canDelete + ? () { + int? relationId = item.cONTACTRELATIONSHIPID; + if (relationId != null) { + showRemoveAlertDialog(context, relationId); + } + } + : null, + child: Container( + height: 38, + decoration: BoxDecoration( + color: canDelete ? const Color(0xFFFFF5F5) : Colors.white, + borderRadius: BorderRadius.circular(10), + border: Border.all(color: canDelete ? const Color(0xFFFFCDD2) : const Color(0xFFE2E8F0)), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.delete_outline_rounded, size: 15, color: canDelete ? const Color(0xFFED1C24) : const Color(0xFF94A3B8)), + const SizedBox(width: 6), + LocaleKeys.remove.tr().toText12(color: canDelete ? const Color(0xFFED1C24) : const Color(0xFF94A3B8)), + ], + ), + ), + ), + ), + ], + ), + ], + ], + ), ), - content: Text(LocaleKeys.updateMember.tr()), - actions: [ - cancelButton, - continueButton, - ], - ); - showDialog( - context: context, - builder: (BuildContext context) { - return alert; - }, ); } - void showRemoveAlertDialog(BuildContext context, relationId) { - Widget cancelButton = TextButton( - child: Text( - LocaleKeys.cancel.tr(), - ), - onPressed: () { - Navigator.pop(context); - }, - ); - Widget continueButton = TextButton( - child: Text( - LocaleKeys.ok.tr(), - ), - onPressed: () { - Navigator.pushNamed(context, AppRoutes.deleteFamilyMember, arguments: relationId); - // continueDynamicForms(); + void showUpdateAlertDialog(BuildContext context, int relationId, int flag, String actionType) { + ConfirmBottomSheet.show( + context, + message: LocaleKeys.updateMember.tr(), + confirmText: LocaleKeys.yes.tr(), + cancelText: LocaleKeys.no.tr(), + onConfirm: () { + Navigator.pushNamed(context, AppRoutes.addUpdateFamilyMember, arguments: FamilyMemberScreenParams(relationId, flag: 2, actionType: "UPDATE")).then((_) => getEmployeeContacts()); }, ); - AlertDialog alert = AlertDialog( - title: Text( - LocaleKeys.confirm.tr(), - ), - content: Text(LocaleKeys.removeThisMember.tr()), - actions: [ - cancelButton, - continueButton, - ], - ); - showDialog( - context: context, - builder: (BuildContext context) { - return alert; + } + + void showRemoveAlertDialog(BuildContext context, int relationId) { + ConfirmBottomSheet.show( + context, + message: LocaleKeys.removeThisMember.tr(), + confirmText: LocaleKeys.yes.tr(), + cancelText: LocaleKeys.no.tr(), + isDestructive: true, + onConfirm: () { + Navigator.pushNamed(context, AppRoutes.deleteFamilyMember, arguments: relationId).then((_) => getEmployeeContacts()); }, ); } diff --git a/lib/ui/profile/mazaya_access_result_screen.dart b/lib/ui/profile/mazaya_access_result_screen.dart new file mode 100644 index 0000000..dfb1ad1 --- /dev/null +++ b/lib/ui/profile/mazaya_access_result_screen.dart @@ -0,0 +1,579 @@ +import 'dart:io'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:mohem_flutter_app/app_state/app_state.dart'; +import 'package:mohem_flutter_app/classes/utils.dart'; +import 'package:mohem_flutter_app/extensions/string_extensions.dart'; +import 'package:mohem_flutter_app/generated/locale_keys.g.dart'; +import 'package:mohem_flutter_app/widgets/button/default_button.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:share_plus/share_plus.dart'; + +class MazayaAccessResultParams { + final bool isGranted; + final String dependentName; + final String? dependentInitials; + final String? downloadUrl; + final String? nationalId; + final String? dateOfBirth; + final String? message; + + const MazayaAccessResultParams({required this.isGranted, required this.dependentName, this.dependentInitials, this.downloadUrl, this.nationalId, this.dateOfBirth, this.message}); +} + +class _ShieldPainter extends CustomPainter { + final Color color; + final bool isGranted; + + _ShieldPainter({required this.color, required this.isGranted}); + + @override + void paint(Canvas canvas, Size size) { + Paint paint = + Paint() + ..color = color + ..style = PaintingStyle.stroke + ..strokeWidth = 2.4 + ..strokeCap = StrokeCap.round + ..strokeJoin = StrokeJoin.round; + + double w = size.width; + double h = size.height; + + // Shield path + Path path = Path(); + path.moveTo(w * 0.5, h * 0.1); + path.cubicTo(w * 0.72, h * 0.1, w * 0.88, h * 0.18, w * 0.88, h * 0.38); + path.cubicTo(w * 0.88, h * 0.65, w * 0.68, h * 0.84, w * 0.5, h * 0.92); + path.cubicTo(w * 0.32, h * 0.84, w * 0.12, h * 0.65, w * 0.12, h * 0.38); + path.cubicTo(w * 0.12, h * 0.18, w * 0.28, h * 0.1, w * 0.5, h * 0.1); + path.close(); + + canvas.drawPath(path, paint); + + if (isGranted) { + // Checkmark path + Path checkPath = Path(); + checkPath.moveTo(w * 0.34, h * 0.49); + checkPath.lineTo(w * 0.46, h * 0.61); + checkPath.lineTo(w * 0.66, h * 0.39); + canvas.drawPath(checkPath, paint); + } else { + // Cross path + canvas.drawLine(Offset(w * 0.38, h * 0.38), Offset(w * 0.62, h * 0.62), paint); + canvas.drawLine(Offset(w * 0.62, h * 0.38), Offset(w * 0.38, h * 0.62), paint); + } + } + + @override + bool shouldRepaint(covariant _ShieldPainter oldDelegate) => color != oldDelegate.color || isGranted != oldDelegate.isGranted; +} + +class MazayaAccessResultScreen extends StatelessWidget { + final bool isGranted; + final String dependentName; + final String? dependentInitials; + final String? downloadUrl; + final String? nationalId; + final String? dateOfBirth; + final String? message; + + const MazayaAccessResultScreen({Key? key, this.isGranted = true, this.dependentName = "", this.dependentInitials, this.downloadUrl, this.nationalId, this.dateOfBirth, this.message}) + : super(key: key); + + static String getDownloadLink() { + int channel = AppState().postParamsObject?.channel ?? 31; + return channel == 33 ? "https://mohemm.hmg.com/mhmg.html" : "https://mohemm.hmg.com/mohem.html"; + } + + // Channel 33 (HMG) gets the HMG-branded guide, everything else gets the CS-branded one - + // same channel check as getDownloadLink() - crossed with the current app language. + static String getShareGuidePdfAssetPath(bool isAr) { + int channel = AppState().postParamsObject?.channel ?? 31; + String brand = channel == 33 ? "HMG" : "CS"; + String lang = isAr ? "ar" : "en"; + return "assets/pdf/Mazaya Access Guide $brand $lang.pdf"; + } + + String _getInitials(String? name) { + if (name == null || name.trim().isEmpty) return "D"; + List parts = name.trim().split(RegExp(r'\s+')).where((String p) => p.isNotEmpty).toList(); + if (parts.isEmpty) return "D"; + if (parts.length == 1) { + return parts.first.length >= 2 ? parts.first.substring(0, 2).toUpperCase() : parts.first.toUpperCase(); + } + return "${parts.first[0]}${parts.last[0]}".toUpperCase(); + } + + @override + Widget build(BuildContext context) { + bool isAr = AppState().isArabic(context); + String fontFamily = isAr ? 'Cairo' : 'Poppins'; + + // Resolve arguments if passed via ModalRoute + Object? routeArgs = ModalRoute.of(context)?.settings.arguments; + bool effectiveGranted = isGranted; + String effectiveName = dependentName; + String? effectiveInitials = dependentInitials; + String? effectiveUrl = downloadUrl; + String? effectiveNationalId = nationalId; + String? effectiveDateOfBirth = dateOfBirth; + String? effectiveMessage = message; + + if (routeArgs is MazayaAccessResultParams) { + effectiveGranted = routeArgs.isGranted; + effectiveName = routeArgs.dependentName; + effectiveInitials = routeArgs.dependentInitials; + effectiveUrl = routeArgs.downloadUrl; + effectiveNationalId = routeArgs.nationalId; + effectiveDateOfBirth = routeArgs.dateOfBirth; + effectiveMessage = routeArgs.message ?? message; + } else if (routeArgs is Map) { + effectiveGranted = routeArgs['isGranted'] ?? isGranted; + effectiveName = routeArgs['dependentName'] ?? dependentName; + effectiveInitials = routeArgs['dependentInitials'] ?? dependentInitials; + effectiveUrl = routeArgs['downloadUrl'] ?? downloadUrl; + effectiveNationalId = routeArgs['nationalId'] ?? nationalId; + effectiveDateOfBirth = routeArgs['dateOfBirth'] ?? dateOfBirth; + effectiveMessage = routeArgs['message'] ?? message; + } + effectiveName = isAr ? "التابع" : "Dependent"; + // if (effectiveName.trim().isEmpty) { + // String fallback = AppState().dependentName ?? ""; + // if (fallback.isNotEmpty) { + // effectiveName = fallback; + // } else { + // effectiveName = isAr ? "التابع" : "Dependent"; + // } + // } + + String linkToShare = effectiveUrl ?? getDownloadLink(); + String initials = effectiveInitials ?? _getInitials(effectiveName); + + return Scaffold( + backgroundColor: Colors.white, + appBar: PreferredSize( + preferredSize: const Size.fromHeight(56), + child: SafeArea( + child: Container( + height: 56, + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + children: [ + InkWell( + borderRadius: BorderRadius.circular(20), + onTap: () => Navigator.pop(context), + child: const Padding(padding: EdgeInsets.all(8.0), child: Icon(Icons.arrow_back_ios_new_rounded, size: 20, color: Color(0xFF1E293B))), + ), + const SizedBox(width: 4), + LocaleKeys.mazayaTitle.tr().toText20(isBold: true, color: const Color(0xFF1E293B)), + const Spacer(), + InkWell( + borderRadius: BorderRadius.circular(20), + onTap: () => Navigator.pop(context), + child: const Padding(padding: EdgeInsets.all(8.0), child: Icon(Icons.close_rounded, size: 24, color: Color(0xFF1E293B))), + ), + ], + ), + ), + ), + ), + body: SafeArea( + child: Column( + children: [ + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.fromLTRB(20, 16, 20, 20), + child: + effectiveGranted + ? _buildGrantedContent(context, isAr, fontFamily, effectiveName, initials, linkToShare) + : _buildRevokedContent(context, isAr, fontFamily, effectiveName, initials, effectiveMessage), + ), + ), + _buildBottomButtons(context, isAr, fontFamily, effectiveGranted, linkToShare, effectiveNationalId ?? "", effectiveDateOfBirth ?? ""), + ], + ), + ), + ); + } + + Widget _buildGrantedContent(BuildContext context, bool isAr, String fontFamily, String name, String initials, String linkToShare) { + return Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox(height: 8), + + // Shield checkmark badge + Container( + width: 80, + height: 80, + decoration: const BoxDecoration(color: Color(0xFFE6F7F2), shape: BoxShape.circle), + child: Center(child: CustomPaint(size: const Size(40, 40), painter: _ShieldPainter(color: const Color(0xFF168A89), isGranted: true))), + ), + + const SizedBox(height: 18), + + // Title + LocaleKeys.mazayaAccessGranted.tr().toText22(isBold: true, isCentered: true, color: const Color(0xFF1E293B)), + + const SizedBox(height: 8), + + // Description + LocaleKeys.mazayaAccessGrantedDesc.tr(args: [name]).toText14(isCenter: true, color: const Color(0xFF64748B)), + + const SizedBox(height: 22), + + // Access is now active banner + Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), + decoration: BoxDecoration(color: const Color(0xFFE8F8F0), borderRadius: BorderRadius.circular(14), border: Border.all(color: const Color(0xFFCEEFE7), width: 1)), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Padding(padding: EdgeInsets.only(top: 2), child: Icon(Icons.check_circle_outline_rounded, color: Color(0xFF168A89), size: 20)), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LocaleKeys.accessIsNowActive.tr().toText14(isBold: true, color: const Color(0xFF168A89)), + const SizedBox(height: 3), + LocaleKeys.accessIsNowActiveDesc.tr(args: [name]).toText12(color: const Color(0xFF64748B)), + ], + ), + ), + ], + ), + ), + + const SizedBox(height: 14), + + // Dependent Card + Container( + width: double.infinity, + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(14), + border: Border.all(color: const Color(0xFFF1F5F9)), + boxShadow: const [BoxShadow(color: Color(0x08000000), blurRadius: 10, offset: Offset(0, 3))], + ), + child: Row( + children: [ + Container( + width: 42, + height: 42, + decoration: const BoxDecoration(color: Color(0xFFE6F7F2), shape: BoxShape.circle), + child: Center(child: initials.toText14(isBold: true, color: const Color(0xFF168A89))), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + name.toText14(isBold: true, maxlines: 1, color: const Color(0xFF1E293B)), + const SizedBox(height: 2), + LocaleKeys.dependentAccessActive.tr().toText12(isBold: true, color: const Color(0xFF168A89)), + ], + ), + ), + const Icon(Icons.lock_open_rounded, color: Color(0xFF168A89), size: 20), + ], + ), + ), + + const SizedBox(height: 14), + + // Shareable App-Download Link Card + Container( + width: double.infinity, + padding: const EdgeInsets.all(14), + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(14), border: Border.all(color: const Color(0xFFE2E8F0))), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 36, + height: 36, + decoration: BoxDecoration(color: const Color(0xFFE8F8F0), borderRadius: BorderRadius.circular(10)), + child: const Center(child: Icon(Icons.smartphone_rounded, color: Color(0xFF168A89), size: 20)), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LocaleKeys.shareableAppDownloadLink.tr().toText14(isBold: true, color: const Color(0xFF1E293B)), + const SizedBox(height: 2), + LocaleKeys.shareableAppDownloadLinkDesc.tr(args: [name]).toText12(color: const Color(0xFF64748B)), + ], + ), + ), + ], + ), + const SizedBox(height: 12), + Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 9), + decoration: BoxDecoration(color: const Color(0xFFF8FAFC), borderRadius: BorderRadius.circular(10), border: Border.all(color: const Color(0xFFE2E8F0))), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LocaleKeys.downloadLink.tr().toText11(isBold: true, color: const Color(0xFF168A89)), + const SizedBox(height: 3), + linkToShare.toText12(maxLine: 1, color: const Color(0xFF334155)), + ], + ), + ), + ], + ), + ), + ], + ); + } + + Widget _buildRevokedContent(BuildContext context, bool isAr, String fontFamily, String name, String initials, [String? message]) { + return Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox(height: 8), + + // Shield bad badge + Container( + width: 80, + height: 80, + decoration: const BoxDecoration(color: Color(0xFFFEECEB), shape: BoxShape.circle), + child: Center(child: CustomPaint(size: const Size(40, 40), painter: _ShieldPainter(color: const Color(0xFFED1C24), isGranted: false))), + ), + + const SizedBox(height: 18), + + // Title + LocaleKeys.mazayaAccessRevoked.tr().toText22(isBold: true, isCentered: true, color: const Color(0xFF1E293B)), + + const SizedBox(height: 8), + + // Description + (message != null && message.trim().isNotEmpty ? message.trim() : LocaleKeys.mazayaAccessRevokedDesc.tr()).toText14(isCenter: true, color: const Color(0xFF64748B)), + + const SizedBox(height: 22), + + // Contact your sponsor banner + Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), + decoration: BoxDecoration(color: const Color(0xFFFFF5F5), borderRadius: BorderRadius.circular(14), border: Border.all(color: const Color(0xFFFFCDD2), width: 1)), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Padding(padding: EdgeInsets.only(top: 2), child: Icon(Icons.phone_outlined, color: Color(0xFFED1C24), size: 20)), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LocaleKeys.contactYourSponsor.tr().toText14(isBold: true, color: const Color(0xFFED1C24)), + const SizedBox(height: 3), + LocaleKeys.contactYourSponsorDesc.tr().toText12(color: const Color(0xFF64748B)), + ], + ), + ), + ], + ), + ), + + const SizedBox(height: 14), + + // Dependent Card + Container( + width: double.infinity, + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(14), + border: Border.all(color: const Color(0xFFF1F5F9)), + boxShadow: const [BoxShadow(color: Color(0x08000000), blurRadius: 10, offset: Offset(0, 3))], + ), + child: Row( + children: [ + Container( + width: 42, + height: 42, + decoration: const BoxDecoration(color: Color(0xFFFFF1F0), shape: BoxShape.circle), + child: Center(child: initials.toText14(isBold: true, color: const Color(0xFFED1C24))), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // name.toText14(isBold: true, maxlines: 1, color: const Color(0xFF1E293B)), + const SizedBox(height: 2), + LocaleKeys.dependentAccessBlocked.tr().toText12(isBold: true, color: const Color(0xFFED1C24)), + ], + ), + ), + const Icon(Icons.lock_outline_rounded, color: Color(0xFFED1C24), size: 20), + ], + ), + ), + ], + ); + } + + // Display-only DOB formatting for the share message: "dd-MM-yyyy", no time component. + // The raw value can come in as the API's "/Date(ms+tz)/" wrapper or a plain ISO datetime string + // with a time component - either way we only want the date part. + String _formatDob(String raw) { + String s = raw.trim(); + if (s.isEmpty) return ""; + + try { + RegExp aspNetDateRegex = RegExp(r'/Date\((-?\d+)([+-]\d{4})?\)/'); + Match? match = aspNetDateRegex.firstMatch(s); + if (match != null) { + int ms = int.parse(match.group(1)!); + return DateFormat('dd-MM-yyyy').format(DateTime.fromMillisecondsSinceEpoch(ms, isUtc: true)); + } + + DateTime? parsed = DateTime.tryParse(s); + if (parsed != null) { + return DateFormat('dd-MM-yyyy').format(parsed); + } + + // DATE_OF_BIRTH comes back as "M/d/yyyy h:mm:ss a" (e.g. "10/25/1997 12:00:00 AM") - + // DateTime.tryParse above doesn't understand that shape, so strip the clock-time suffix + // and parse the date part explicitly. MM/dd/yyyy before dd/MM/yyyy since the real source + // is confirmed month-first - an ambiguous case like "5/6/1990" must resolve to May 6. + String dateOnly = s.replaceFirst(RegExp(r'\s+\d{1,2}:\d{2}(:\d{2})?\s*(AM|PM)?$', caseSensitive: false), '').trim(); + for (String fmt in ['MM/dd/yyyy', 'dd/MM/yyyy', 'yyyy-MM-dd', 'dd-MM-yyyy']) { + try { + DateTime dateOnlyParsed = DateFormat(fmt, 'en_US').parseStrict(dateOnly); + return DateFormat('dd-MM-yyyy').format(dateOnlyParsed); + } catch (_) {} + } + } catch (_) {} + + // Fallback: strip a time component off a plain "yyyy-MM-dd HH:mm:ss" / "yyyy-MM-ddTHH:mm:ss" + // string we couldn't otherwise parse, rather than showing it as-is with the time attached. + if (s.contains('T')) return s.split('T').first.replaceAll('/', '-'); + if (s.contains(' ')) return s.split(' ').first.replaceAll('/', '-'); + return s.replaceAll('/', '-'); + } + + // Same message built for both "Share Download Link" and "Copy Link" - they must always + // carry identical content, so this is the single source of truth for it. + String _buildMazayaInviteMessage(String linkToShare, String nationalId, String dateOfBirth) { + String nationalIdLine = nationalId.trim().isEmpty ? LocaleKeys.nationalIdOrIqamaFallback.tr() : "${LocaleKeys.nationalId.tr()}: ${nationalId.replaceAll('-', '')}"; + return "$linkToShare\n\n${LocaleKeys.mazayaInviteInstructions.tr()}\n\n$nationalIdLine\n${LocaleKeys.dateOfBirth.tr()}: ${_formatDob(dateOfBirth)}"; + } + + // Text-only share (the invite message + link). Kept separate from the guide share below + // because some targets (e.g. WhatsApp) drop the accompanying `text` entirely once a file is + // attached to the same share - splitting them guarantees the message itself is always visible. + Future _shareMazayaInviteLink(bool isAr, String linkToShare, String nationalId, String dateOfBirth) async { + String message = _buildMazayaInviteMessage(linkToShare, nationalId, dateOfBirth); + try { + await SharePlus.instance.share(ShareParams(text: message)); + } catch (_) {} + } + + // File-only share of the bundled access guide PDF - no accompanying text, so every target + // (including WhatsApp) just attaches the file with nothing to drop. + Future _shareMazayaAccessGuide(bool isAr) async { + try { + ByteData bytes = await rootBundle.load(getShareGuidePdfAssetPath(isAr)); + Directory tempDir = await getTemporaryDirectory(); + // Write to a space-free filename locally - the asset path above is what actually matters. + String localFileName = isAr ? "mazaya_access_guide_ar.pdf" : "mazaya_access_guide_en.pdf"; + File pdfFile = File('${tempDir.path}/$localFileName'); + await pdfFile.writeAsBytes(bytes.buffer.asUint8List(bytes.offsetInBytes, bytes.lengthInBytes)); + + await SharePlus.instance.share(ShareParams(files: [XFile(pdfFile.path)])); + } catch (_) {} + } + + + + Widget _buildBottomButtons(BuildContext context, bool isAr, String fontFamily, bool granted, String linkToShare, String nationalId, String dateOfBirth) { + if (granted) { + return Container( + color: Colors.white, + padding: const EdgeInsets.fromLTRB(20, 10, 20, 20), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + DefaultButton( + LocaleKeys.shareDownloadLink.tr(), + () => _shareMazayaInviteLink(isAr, linkToShare, nationalId, dateOfBirth), + trailingIconData: isAr ? Icons.arrow_back_rounded : Icons.arrow_forward_rounded, + fontSize: 14, + isTextExpanded: false, + height: 48, + borderRadius: 12, + ), + const SizedBox(height: 10), + DefaultButton( + LocaleKeys.shareAccessGuide.tr(), + () => _shareMazayaAccessGuide(isAr), + trailingIconData: isAr ? Icons.arrow_back_rounded : Icons.arrow_forward_rounded, + fontSize: 14, + isTextExpanded: false, + height: 48, + borderRadius: 12, + ), + const SizedBox(height: 10), + DefaultButton( + LocaleKeys.copyLink.tr(), + () { + Clipboard.setData(ClipboardData(text: _buildMazayaInviteMessage(linkToShare, nationalId, dateOfBirth))); + Utils.showToast(LocaleKeys.linkCopied.tr(), isSuccess: true); + }, + colors: const [Colors.white, Colors.white], + border: Border.all(color: const Color(0xFFCBD5E1)), + textColor: const Color(0xFF168A89), + fontSize: 14, + isTextExpanded: false, + height: 48, + borderRadius: 12, + ), + const SizedBox(height: 10), + DefaultButton( + LocaleKeys.done.tr(), + () => Navigator.pop(context), + colors: const [Colors.white, Colors.white], + border: Border.all(color: const Color(0xFFCBD5E1)), + textColor: const Color(0xFF334155), + fontSize: 14, + isTextExpanded: false, + height: 48, + borderRadius: 12, + ), + ], + ), + ); + } else { + return Container( + color: Colors.white, + padding: const EdgeInsets.fromLTRB(20, 10, 20, 20), + child: DefaultButton( + LocaleKeys.returnToSignIn.tr(), + () => Navigator.pop(context), + colors: const [Colors.white, Colors.white], + border: Border.all(color: const Color(0xFFCEEFE7), width: 1.2), + textColor: const Color(0xFF168A89), + iconData: isAr ? Icons.arrow_forward_rounded : Icons.arrow_back_rounded, + fontSize: 14, + isTextExpanded: false, + height: 48, + borderRadius: 12, + ), + ); + } + } +} diff --git a/lib/ui/screens/items_for_sale/fragments/item_review_fragment.dart b/lib/ui/screens/items_for_sale/fragments/item_review_fragment.dart index 1e3581a..de67d2e 100644 --- a/lib/ui/screens/items_for_sale/fragments/item_review_fragment.dart +++ b/lib/ui/screens/items_for_sale/fragments/item_review_fragment.dart @@ -146,7 +146,7 @@ class _ItemReviewFragmentState extends State { Utils.showLoading(context); String message = await ItemsForSaleApiClient().addItemForSale(itemReviewModel!, imagesList); Utils.hideLoading(context); - Utils.showToast(message); + Utils.showToast(message, isSuccess: true); Navigator.of(context).pop(); setState(() {}); } catch (ex) { diff --git a/lib/ui/screens/mowadhafhi/mowadhafhi_hr_request.dart b/lib/ui/screens/mowadhafhi/mowadhafhi_hr_request.dart index 498581a..0622444 100644 --- a/lib/ui/screens/mowadhafhi/mowadhafhi_hr_request.dart +++ b/lib/ui/screens/mowadhafhi/mowadhafhi_hr_request.dart @@ -314,7 +314,7 @@ class _MowadhafhiHRRequestState extends State { } int? messageStatus = await MowadhafhiApiClient().submitRequest(selectedDepartment?.projectDepartmentId, description, projectID, selectedSection?.departmentSectionId.toString(), selectedTopic?.sectionTopicId.toString(), selectedServiceType!.ticketTypeId, list); - Utils.showToast(LocaleKeys.requestCreatedSuccessfully.tr()); + Utils.showToast(LocaleKeys.requestCreatedSuccessfully.tr(), isSuccess: true); Utils.hideLoading(context); Navigator.pop(context); } catch (ex) { diff --git a/lib/ui/screens/mowadhafhi/request_details.dart b/lib/ui/screens/mowadhafhi/request_details.dart index a104f51..ec63f67 100644 --- a/lib/ui/screens/mowadhafhi/request_details.dart +++ b/lib/ui/screens/mowadhafhi/request_details.dart @@ -17,7 +17,6 @@ import 'package:mohem_flutter_app/models/mowadhafhi/get_ticket_transactions.dart import 'package:mohem_flutter_app/models/mowadhafhi/get_transaction_attachment_model.dart'; import 'package:mohem_flutter_app/ui/screens/mowadhafhi/view_transaction_attachment.dart'; import 'package:mohem_flutter_app/widgets/app_bar_widget.dart'; -import 'package:mohem_flutter_app/widgets/dialogs/confirm_dialog.dart'; import 'package:open_filex/open_filex.dart'; import 'package:path_provider/path_provider.dart'; import 'package:permission_handler/permission_handler.dart'; @@ -283,15 +282,7 @@ class _RequestDetailsState extends State { Utils.showToast("Cannot open file."); } } else { - showDialog( - context: context, - builder: (BuildContext cxt) => ConfirmDialog( - message: "You need to give storage permission to view files.", - onTap: () { - Navigator.pop(context); - }, - ), - ); + Utils.showToast("You need to give storage permission to view files."); } }); } diff --git a/lib/ui/screens/offers_and_discounts/offers_and_discounts_home.dart b/lib/ui/screens/offers_and_discounts/offers_and_discounts_home.dart index dbc2366..407506f 100644 --- a/lib/ui/screens/offers_and_discounts/offers_and_discounts_home.dart +++ b/lib/ui/screens/offers_and_discounts/offers_and_discounts_home.dart @@ -61,62 +61,58 @@ class _OffersAndDiscountsHomeState extends State { LocaleKeys.browseCategories.tr().toText17().paddingOnly(left: 21, right: 21, top: 21), SizedBox( height: 110.0, - child: getCategoriesList.isNotEmpty - ? ListView.separated( - shrinkWrap: true, - physics: const BouncingScrollPhysics(), - padding: const EdgeInsets.only(left: 21, right: 21, top: 13, bottom: 13), - scrollDirection: Axis.horizontal, - itemBuilder: (cxt, index) { - return AspectRatio( - aspectRatio: 1 / 1, - child: InkWell( - onTap: () { - setState(() { - currentCategoryID = getCategoriesList[index].id!; - getCategoryOffersListAPI(); - // getItemsForSaleList.clear(); - // currentPageNo = 1; - // getItemsForSale(currentPageNo, currentCategoryID); - }); - }, - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SvgPicture.string( - getCategoriesList[index].content!, - fit: BoxFit.contain, - width: 25, - height: 25, - ), - currentCategoryID == getCategoriesList[index].id ? const Icon(Icons.check_circle_rounded, color: MyColors.greenColor, size: 16.0) : Container(), - ], - ).expanded, - AppState().isArabic(context) ? getCategoriesList[index].categoryNameAr!.toText10() : getCategoriesList[index].categoryNameEn!.toText10() - ], - ).paddingOnly(left: 10, right: 10, bottom: 10, top: 12).expanded.objectContainerView(disablePadding: true), - ), - ); - }, - separatorBuilder: (cxt, index) => 12.width, - itemCount: getCategoriesList.length, - ) - : Container(), + child: + getCategoriesList.isNotEmpty + ? ListView.separated( + shrinkWrap: true, + physics: const BouncingScrollPhysics(), + padding: const EdgeInsets.only(left: 21, right: 21, top: 13, bottom: 13), + scrollDirection: Axis.horizontal, + itemBuilder: (cxt, index) { + return AspectRatio( + aspectRatio: 1 / 1, + child: InkWell( + onTap: () { + setState(() { + currentCategoryID = getCategoriesList[index].id!; + getCategoryOffersListAPI(); + // getItemsForSaleList.clear(); + // currentPageNo = 1; + // getItemsForSale(currentPageNo, currentCategoryID); + }); + }, + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SvgPicture.string(getCategoriesList[index].content!, fit: BoxFit.contain, width: 25, height: 25), + currentCategoryID == getCategoriesList[index].id ? const Icon(Icons.check_circle_rounded, color: MyColors.greenColor, size: 16.0) : Container(), + ], + ).expanded, + AppState().isArabic(context) ? getCategoriesList[index].categoryNameAr!.toText10() : getCategoriesList[index].categoryNameEn!.toText10(), + ], + ).paddingOnly(left: 10, right: 10, bottom: 10, top: 12).expanded.objectContainerView(disablePadding: true), + ), + ); + }, + separatorBuilder: (cxt, index) => 12.width, + itemCount: getCategoriesList.length, + ) + : Container(), ), getOffersList.isNotEmpty ? GridView( - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2, childAspectRatio: 162 / 266, crossAxisSpacing: 12, mainAxisSpacing: 12), - padding: const EdgeInsets.only(left: 21, right: 21, bottom: 21, top: 21), - shrinkWrap: true, - primary: false, - physics: const ScrollPhysics(), - children: getItemsForSaleWidgets(), - ) + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2, childAspectRatio: 162 / 266, crossAxisSpacing: 12, mainAxisSpacing: 12), + padding: const EdgeInsets.only(left: 21, right: 21, bottom: 21, top: 21), + shrinkWrap: true, + primary: false, + physics: const ScrollPhysics(), + children: getItemsForSaleWidgets(), + ) : Utils.getNoDataWidget(context).paddingOnly(top: 50), ], ), @@ -144,13 +140,7 @@ class _OffersAndDiscountsHomeState extends State { decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(10), - boxShadow: [ - BoxShadow( - color: const Color(0xff000000).withOpacity(.05), - blurRadius: 26, - offset: const Offset(0, -3), - ), - ], + boxShadow: [BoxShadow(color: const Color(0xff000000).withOpacity(.05), blurRadius: 26, offset: const Offset(0, -3))], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -159,16 +149,7 @@ class _OffersAndDiscountsHomeState extends State { Hero( tag: "ItemImage" + getOffersList.offersDiscountId.toString()!, transitionOnUserGestures: true, - child: AspectRatio( - aspectRatio: 118 / 127, - child: ClipRRect( - borderRadius: BorderRadius.circular(6), - child: Image.network( - getOffersList.bannerImage ?? "", - fit: BoxFit.contain, - ), - ), - ), + child: AspectRatio(aspectRatio: 118 / 127, child: ClipRRect(borderRadius: BorderRadius.circular(6), child: Image.network(getOffersList.bannerImage ?? "", fit: BoxFit.contain))), ), AppState().isArabic(context) ? getOffersList.titleAr!.toText16(isBold: true, color: const Color(0xff2B353E), maxlines: 1) @@ -183,10 +164,7 @@ class _OffersAndDiscountsHomeState extends State { // 8.height, getOffersList.discountDescription!.toText14(isBold: true, maxlines: 1), 20.height, - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [checkDate(getOffersList.endDate!), SvgPicture.asset("assets/images/arrow_next.svg").paddingOnly(bottom: 4)], - ), + Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [checkDate(getOffersList.endDate!), SvgPicture.asset("assets/images/arrow_next.svg").paddingOnly(bottom: 4)]), ], ), ), diff --git a/lib/ui/screens/ticket/ticket_detailed_screen.dart b/lib/ui/screens/ticket/ticket_detailed_screen.dart index 0dcc4c3..7d9cdb0 100644 --- a/lib/ui/screens/ticket/ticket_detailed_screen.dart +++ b/lib/ui/screens/ticket/ticket_detailed_screen.dart @@ -105,7 +105,7 @@ class _TicketDetailedScreenState extends State { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - item.accuralPlanCode.toString().replaceAll('_', ' ').toCamelCase.toText14(), + item.accuralPlanCode.toString().replaceAll('_', ' ').toCamelCase.toText12(), item.accrualNetEntitlement.toString().toText16(color: MyColors.textMixColor, isBold: true), ], ), diff --git a/lib/ui/work_list/itg_detail_screen.dart b/lib/ui/work_list/itg_detail_screen.dart index fdfe19c..1caba54 100644 --- a/lib/ui/work_list/itg_detail_screen.dart +++ b/lib/ui/work_list/itg_detail_screen.dart @@ -480,7 +480,7 @@ class _ItgDetailScreenState extends State { AppState().memberInformationList?.eMPLOYEEEMAILADDRESS ?? "", ); Utils.hideLoading(context); - Utils.showToast(LocaleKeys.yourChangeHasBeenSavedSuccessfully.tr()); + Utils.showToast(LocaleKeys.yourChangeHasBeenSavedSuccessfully.tr(), isSuccess: true); // Navigator.pop(context, "delegate_reload"); AppState().requestAllList!.removeAt(AppState().itgWorkListIndex!); if (AppState().requestAllList!.isEmpty) { @@ -504,7 +504,7 @@ class _ItgDetailScreenState extends State { Utils.showLoading(context); ITGRequest? itgRequest = await WorkListApiClient().answerITGRequest(requestType, taskId, itemId, employeeNumber, "", comments); Utils.hideLoading(context); - Utils.showToast(LocaleKeys.yourChangeHasBeenSavedSuccessfully.tr()); + Utils.showToast(LocaleKeys.yourChangeHasBeenSavedSuccessfully.tr(), isSuccess: true); // Navigator.pop(context, "delegate_reload"); AppState().requestAllList!.removeAt(AppState().itgWorkListIndex!); if (AppState().requestAllList!.isEmpty) { @@ -528,7 +528,7 @@ class _ItgDetailScreenState extends State { Utils.showLoading(context); ITGRequest? itgRequest = await WorkListApiClient().rejectITGRequest(requestType, taskId, itemId, employeeNumber, comments); Utils.hideLoading(context); - Utils.showToast(LocaleKeys.yourChangeHasBeenSavedSuccessfully.tr()); + Utils.showToast(LocaleKeys.yourChangeHasBeenSavedSuccessfully.tr(), isSuccess: true); // Navigator.pop(context, "delegate_reload"); AppState().requestAllList!.removeAt(AppState().itgWorkListIndex!); if (AppState().requestAllList!.isEmpty) { @@ -552,7 +552,7 @@ class _ItgDetailScreenState extends State { Utils.showLoading(context); ITGRequest? itgRequest = await WorkListApiClient().approveITGRequest(requestType, taskId, itemId, employeeNumber, comments); Utils.hideLoading(context); - Utils.showToast(LocaleKeys.yourChangeHasBeenSavedSuccessfully.tr()); + Utils.showToast(LocaleKeys.yourChangeHasBeenSavedSuccessfully.tr(), isSuccess: true); // Navigator.pop(context, "delegate_reload"); animationIndex = animationIndex + 1; AppState().requestAllList!.removeAt(AppState().itgWorkListIndex!); @@ -578,7 +578,7 @@ class _ItgDetailScreenState extends State { animationIndex = animationIndex + 1; ITGRequest? itgRequest = await WorkListApiClient().grantITGRequest(requestType, taskId, itemId, employeeNumber, "", ""); Utils.hideLoading(context); - Utils.showToast(LocaleKeys.yourChangeHasBeenSavedSuccessfully.tr()); + Utils.showToast(LocaleKeys.yourChangeHasBeenSavedSuccessfully.tr(), isSuccess: true); AppState().requestAllList!.removeAt(AppState().itgWorkListIndex!); if (AppState().requestAllList!.isEmpty) { diff --git a/lib/ui/work_list/sheets/selected_item_sheet.dart b/lib/ui/work_list/sheets/selected_item_sheet.dart index 9b98c8b..2a85fa4 100644 --- a/lib/ui/work_list/sheets/selected_item_sheet.dart +++ b/lib/ui/work_list/sheets/selected_item_sheet.dart @@ -212,13 +212,19 @@ class SelectedItemSheet extends StatelessWidget { var requestDetails = AppState().requestAllList![AppState().itgWorkListIndex!]; if (apiMode == "Delegate") { await WorkListApiClient().delegateITGRequest(requestDetails.requestType!, requestDetails.iD, requestDetails.itemID, AppState().memberInformationList?.eMPLOYEENUMBER ?? "", - isITGRequest ? favoriteReplacements!.userName! : actionHistoryList!.uSERNAME!, comment); + //commented the previous one because we need the replacement in both cases + favoriteReplacements !=null ? favoriteReplacements!.userName! : actionHistoryList!.uSERNAME!, comment); + // isITGRequest ? favoriteReplacements!.userName! : actionHistoryList!.uSERNAME!, comment); } else if (apiMode == "RequestInformation") { await WorkListApiClient().informationITGRequest(requestDetails.requestType!, requestDetails.iD, requestDetails.itemID, AppState().memberInformationList?.eMPLOYEENUMBER ?? "", - isITGRequest ? favoriteReplacements!.userName! : actionHistoryList!.uSERNAME!, comment); + //commented the previous one because we need the replacement in both cases + // isITGRequest ? favoriteReplacements!.userName! : actionHistoryList!.uSERNAME!, comment); + favoriteReplacements !=null ? favoriteReplacements!.userName! : actionHistoryList!.uSERNAME!, comment); } else if (apiMode == "Answer") { await WorkListApiClient().answerITGRequest(requestDetails.requestType!, requestDetails.iD, requestDetails.itemID, AppState().memberInformationList?.eMPLOYEENUMBER ?? "", - isITGRequest ? favoriteReplacements!.userName! : actionHistoryList!.uSERNAME!, comment); + //commented the previous one because we need the replacement in both cases + favoriteReplacements !=null ? favoriteReplacements!.userName! : actionHistoryList!.uSERNAME!, comment); + //favoriteReplacements!.userName! : actionHistoryList!.uSERNAME!, comment); } Utils.hideLoading(context); Navigator.pop(context, "delegate_reload"); diff --git a/lib/ui/work_list/worklist_detail_screen.dart b/lib/ui/work_list/worklist_detail_screen.dart index 6bf8ea9..9a9ccee 100644 --- a/lib/ui/work_list/worklist_detail_screen.dart +++ b/lib/ui/work_list/worklist_detail_screen.dart @@ -46,7 +46,7 @@ import 'package:mohem_flutter_app/widgets/app_bar_widget.dart'; import 'package:mohem_flutter_app/widgets/bottom_sheet.dart'; import 'package:mohem_flutter_app/widgets/button/default_button.dart'; import 'package:mohem_flutter_app/widgets/dialogs/accept_reject_input_dialog.dart'; -import 'package:mohem_flutter_app/widgets/dialogs/confirm_dialog.dart'; +import 'package:mohem_flutter_app/widgets/bottom_sheets/confirm_bottom_sheet.dart'; class WorkListDetailScreen extends StatefulWidget { WorkListDetailScreen({Key? key}) : super(key: key); @@ -606,24 +606,23 @@ class _WorkListDetailScreenState extends State { } Future performNetworkCall(BuildContext context, {String? email, String? userId}) async { - showDialog( - context: context, - builder: (BuildContext cxt) => ConfirmDialog( - message: LocaleKeys.wantToReject.tr(), - okTitle: LocaleKeys.reject.tr(), - onTap: () async { - Navigator.pop(cxt); - Utils.showLoading(context); - try { - await WorkListApiClient().submitComment(comment: "", email: email, userId: userId, notificationId: workListData!.nOTIFICATIONID, apiMode: "REJECT"); - Utils.hideLoading(context); - Navigator.pop(context, "delegate_reload"); - } catch (ex) { - Utils.hideLoading(context); - Utils.handleException(ex, context, null); - } - }, - ), + ConfirmBottomSheet.show( + context, + message: LocaleKeys.wantToReject.tr(), + confirmText: LocaleKeys.reject.tr(), + cancelText: LocaleKeys.cancel.tr(), + isDestructive: true, + onConfirm: () async { + Utils.showLoading(context); + try { + await WorkListApiClient().submitComment(comment: "", email: email, userId: userId, notificationId: workListData!.nOTIFICATIONID, apiMode: "REJECT"); + Utils.hideLoading(context); + Navigator.pop(context, "delegate_reload"); + } catch (ex) { + Utils.hideLoading(context); + Utils.handleException(ex, context, null); + } + }, ); } @@ -743,7 +742,7 @@ class _WorkListDetailScreenState extends State { Utils.showLoading(context); GenericResponseModel model = await WorkListApiClient().postNotificationActions(payload); Utils.hideLoading(context); - Utils.showToast(LocaleKeys.yourChangeHasBeenSavedSuccessfully.tr()); + Utils.showToast(LocaleKeys.yourChangeHasBeenSavedSuccessfully.tr(), isSuccess: true); animationIndex = animationIndex + 1; AppState().workList!.removeAt(AppState().workListIndex!); if (AppState().workList!.isEmpty) { diff --git a/lib/widgets/app_bar_widget.dart b/lib/widgets/app_bar_widget.dart index 277a414..e25165b 100644 --- a/lib/widgets/app_bar_widget.dart +++ b/lib/widgets/app_bar_widget.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:mohem_flutter_app/classes/colors.dart'; +import 'package:mohem_flutter_app/classes/consts.dart'; import 'package:mohem_flutter_app/config/routes.dart'; import 'package:mohem_flutter_app/extensions/int_extensions.dart'; import 'package:mohem_flutter_app/extensions/string_extensions.dart'; @@ -10,6 +11,7 @@ AppBar AppBarWidget( BuildContext context, { required String title, bool showHomeButton = true, + bool showBackButton = true, bool showLogo = false, String? logoPath, bool showWorkListSettingButton = false, @@ -24,17 +26,24 @@ AppBar AppBarWidget( surfaceTintColor: Colors.transparent, title: Row( children: [ - GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: Feedback.wrapForTap(() { - (onBackTapped == null ? Navigator.maybePop(context) : onBackTapped()); - }, context), - child: const Icon(Icons.arrow_back_ios, color: MyColors.darkIconColor), - ), + if (showBackButton) + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: Feedback.wrapForTap(() { + (onBackTapped == null + ? Navigator.maybePop(context) + : onBackTapped()); + }, context), + child: const Icon( + Icons.arrow_back_ios, + color: MyColors.darkIconColor, + ), + ), if (showLogo) 4.width, if (showLogo) SvgPicture.asset(logoPath!), 4.width, - if (!showLogo)title.toText24(color: MyColors.darkTextColor, isBold: true).expanded, + if (!showLogo) + title.toText24(color: MyColors.darkTextColor, isBold: true).expanded, ], ), centerTitle: false, @@ -44,7 +53,18 @@ AppBar AppBarWidget( if (showHomeButton) IconButton( onPressed: () { - onHomeTapped == null ? Navigator.popUntil(context, ModalRoute.withName(AppRoutes.dashboard)) : onHomeTapped(); + if (onHomeTapped != null) { + onHomeTapped(); + } else { + Navigator.popUntil( + context, + ModalRoute.withName( + ApiConsts.isDependetLogin + ? AppRoutes.mazayaDiscover + : AppRoutes.dashboard, + ), + ); + } }, icon: const Icon(Icons.home, color: MyColors.darkIconColor), ), @@ -53,7 +73,10 @@ AppBar AppBarWidget( onPressed: () { Navigator.pushNamed(context, AppRoutes.worklistSettings); }, - icon: const Icon(Icons.settings_rounded, color: MyColors.darkIconColor), + icon: const Icon( + Icons.settings_rounded, + color: MyColors.darkIconColor, + ), ), if (showMemberButton) IconButton( diff --git a/lib/widgets/app_in_app_webview_screen.dart b/lib/widgets/app_in_app_webview_screen.dart new file mode 100644 index 0000000..e0ee934 --- /dev/null +++ b/lib/widgets/app_in_app_webview_screen.dart @@ -0,0 +1,177 @@ +import 'dart:async'; + +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_inappwebview/flutter_inappwebview.dart'; +import 'package:mohem_flutter_app/classes/consts.dart'; +import 'package:mohem_flutter_app/config/routes.dart'; +import 'package:mohem_flutter_app/generated/locale_keys.g.dart'; +import 'package:mohem_flutter_app/widgets/app_bar_widget.dart'; +import 'package:permission_handler/permission_handler.dart'; + +class AppInAppWebViewScreen extends StatefulWidget { + final String url; + final String? title; + final void Function(InAppWebViewController controller)? onPageLoaded; + + const AppInAppWebViewScreen({ + Key? key, + required this.url, + this.title, + this.onPageLoaded, + }) : super(key: key); + + @override + State createState() => _AppInAppWebViewScreenState(); +} + +class _AppInAppWebViewScreenState extends State { + InAppWebViewController? _webViewController; + double _progress = 0; + String _title = ""; + + @override + void initState() { + super.initState(); + _title = widget.title ?? ""; + _requestCameraPermission(); + } + + Future _requestCameraPermission() async { + try { + PermissionStatus cameraStatus = await Permission.camera.status; + if (!cameraStatus.isGranted) { + await Permission.camera.request(); + } + } catch (e) { + debugPrint("Error requesting camera permission: $e"); + } + } + + void _onHomeTapped() { + if (ApiConsts.isDependetLogin) { + Navigator.popUntil(context, ModalRoute.withName(AppRoutes.mazayaDiscover)); + } else { + Navigator.popUntil(context, ModalRoute.withName(AppRoutes.dashboard)); + } + } + + Future _handleBack() async { + if (_webViewController != null && await _webViewController!.canGoBack()) { + await _webViewController!.goBack(); + } else { + if (mounted) { + Navigator.pop(context); + } + } + } + + @override + Widget build(BuildContext context) { + String displayTitle = _title.isNotEmpty ? _title : LocaleKeys.offerDetails.tr(); + + return PopScope( + canPop: false, + onPopInvokedWithResult: (bool didPop, Object? result) async { + if (didPop) return; + await _handleBack(); + }, + child: Scaffold( + backgroundColor: Colors.white, + appBar: AppBarWidget( + context, + title: displayTitle, + showHomeButton: true, + onHomeTapped: _onHomeTapped, + onBackTapped: () async { + await _handleBack(); + }, + ), + body: SafeArea( + child: Stack( + children: [ + InAppWebView( + initialUrlRequest: URLRequest(url: WebUri(widget.url)), + initialSettings: InAppWebViewSettings( + javaScriptEnabled: true, + domStorageEnabled: true, + supportZoom: true, + mediaPlaybackRequiresUserGesture: false, + allowsInlineMediaPlayback: true, + javaScriptCanOpenWindowsAutomatically: true, + transparentBackground: false, + useShouldOverrideUrlLoading: false, + cacheEnabled: true, + clearCache: false, + ), + onWebViewCreated: (InAppWebViewController controller) { + _webViewController = controller; + }, + onPermissionRequest: (InAppWebViewController controller, PermissionRequest permissionRequest) async { + debugPrint("InAppWebView onPermissionRequest: ${permissionRequest.resources}"); + bool isGranted = false; + try { + PermissionStatus status = await Permission.camera.status; + isGranted = status.isGranted; + // IMPORTANT: never await Permission.camera.request() here. The native WebView + // is blocked waiting on THIS callback's response to the page's getUserMedia() + // call - if we hold it open for however long the human takes to respond to the + // OS dialog (can be 30s+), Chromium abandons the original getUserMedia() call + // as timed out before we ever answer. So: answer immediately with whatever the + // status already is, and if not granted, kick off the slow dialog in the + // background. Once that resolves, retry the click trigger so the page makes a + // FRESH getUserMedia() call - which resolves instantly next time since the OS + // permission is already durably granted by then (no dialog, no timeout risk). + if (!isGranted) { + unawaited( + Permission.camera.request().then((PermissionStatus result) { + if (result.isGranted) { + widget.onPageLoaded?.call(controller); + } + }), + ); + } + } catch (e) { + debugPrint("Error checking camera permission: $e"); + } + return PermissionResponse( + resources: permissionRequest.resources, + action: isGranted ? PermissionResponseAction.GRANT : PermissionResponseAction.DENY, + ); + }, + onProgressChanged: (InAppWebViewController controller, int progress) { + if (mounted) { + setState(() { + _progress = progress / 100; + }); + } + }, + onTitleChanged: (InAppWebViewController controller, String? title) { + if (widget.title == null || widget.title!.isEmpty) { + if (title != null && title.isNotEmpty && !title.startsWith("http")) { + if (mounted) { + setState(() { + _title = title; + }); + } + } + } + }, + onLoadStop: (InAppWebViewController controller, WebUri? url) async { + widget.onPageLoaded?.call(controller); + }, + ), + if (_progress < 1.0) + LinearProgressIndicator( + value: _progress, + color: const Color(0xFF168A89), + backgroundColor: Colors.transparent, + minHeight: 2.5, + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/widgets/bottom_sheets/attachment_options.dart b/lib/widgets/bottom_sheets/attachment_options.dart index 30189a0..b649937 100644 --- a/lib/widgets/bottom_sheets/attachment_options.dart +++ b/lib/widgets/bottom_sheets/attachment_options.dart @@ -1,8 +1,10 @@ +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:mohem_flutter_app/classes/colors.dart'; import 'package:mohem_flutter_app/extensions/string_extensions.dart'; import 'package:mohem_flutter_app/extensions/widget_extensions.dart'; +import 'package:mohem_flutter_app/generated/locale_keys.g.dart'; class AttachmentOptions extends StatelessWidget { VoidCallback onCameraTap; @@ -19,16 +21,16 @@ class AttachmentOptions extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - "Upload Attachment".toSectionHeading(), - "Select from gallery or open camera".toText11(weight: FontWeight.w500), + LocaleKeys.uploadAttachment.tr().toSectionHeading(), + LocaleKeys.selectFromGalleryOrOpenCamera.tr().toText11(weight: FontWeight.w500), GridView( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3, childAspectRatio: 105 / 105, crossAxisSpacing: 9, mainAxisSpacing: 9), physics: const NeverScrollableScrollPhysics(), padding: const EdgeInsets.only(top: 21, bottom: 14), shrinkWrap: true, children: [ - itemView("open_camera.svg", "Open\nCamera", onCameraTap), - itemView("gallery.svg", "Upload from\nGallery", onGalleryTap), + itemView("open_camera.svg", LocaleKeys.openCamera.tr(), onCameraTap), + itemView("gallery.svg", LocaleKeys.uploadFromGallery.tr(), onGalleryTap), if (showFilesOption) itemView("files.svg", "Upload from\nFiles", onFilesTap), ], ) diff --git a/lib/widgets/bottom_sheets/confirm_bottom_sheet.dart b/lib/widgets/bottom_sheets/confirm_bottom_sheet.dart new file mode 100644 index 0000000..850fcf4 --- /dev/null +++ b/lib/widgets/bottom_sheets/confirm_bottom_sheet.dart @@ -0,0 +1,173 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:mohem_flutter_app/classes/colors.dart'; +import 'package:mohem_flutter_app/extensions/int_extensions.dart'; +import 'package:mohem_flutter_app/extensions/string_extensions.dart'; +import 'package:mohem_flutter_app/generated/locale_keys.g.dart'; +import 'package:mohem_flutter_app/widgets/button/default_button.dart'; + +/// Generic, fully customizable confirmation bottom sheet matching the app's visual theme +/// (same handle bar / rounded-top-sheet / gradient button look used across other sheets). +/// Everything is driven by the caller - title/message/button text/colors/icon/callbacks - +/// with sensible defaults so a minimal call still looks consistent with the rest of the app. +class ConfirmBottomSheet extends StatelessWidget { + final String? title; + final String message; + final String? confirmText; + final String? cancelText; + final VoidCallback onConfirm; + final VoidCallback? onCancel; + final bool isDestructive; + final bool showCancelButton; + final IconData? icon; + final Color? iconColor; + final Color? iconBackgroundColor; + final List? confirmButtonColors; + final Color? confirmButtonTextColor; + final Color? cancelButtonColor; + final Color? cancelButtonTextColor; + // Optional custom content (e.g. an avatar/photo) rendered below the message, above the + // buttons - for cases that need more than the single IconData `icon` slot can show. + final Widget? avatar; + + const ConfirmBottomSheet({ + Key? key, + this.title, + required this.message, + this.confirmText, + this.cancelText, + required this.onConfirm, + this.onCancel, + this.isDestructive = false, + this.showCancelButton = true, + this.icon, + this.iconColor, + this.iconBackgroundColor, + this.confirmButtonColors, + this.confirmButtonTextColor, + this.cancelButtonColor, + this.cancelButtonTextColor, + this.avatar, + }) : super(key: key); + + /// Convenience launcher - mirrors Utils.confirmDialog's call shape but as a bottom sheet. + static Future show( + BuildContext context, { + String? title, + required String message, + String? confirmText, + String? cancelText, + required VoidCallback onConfirm, + VoidCallback? onCancel, + bool isDestructive = false, + bool showCancelButton = true, + IconData? icon, + Color? iconColor, + Color? iconBackgroundColor, + List? confirmButtonColors, + Color? confirmButtonTextColor, + Color? cancelButtonColor, + Color? cancelButtonTextColor, + Widget? avatar, + }) { + return showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (BuildContext sheetContext) { + return ConfirmBottomSheet( + title: title, + message: message, + confirmText: confirmText, + cancelText: cancelText, + onConfirm: onConfirm, + onCancel: onCancel, + isDestructive: isDestructive, + showCancelButton: showCancelButton, + icon: icon, + iconColor: iconColor, + iconBackgroundColor: iconBackgroundColor, + confirmButtonColors: confirmButtonColors, + confirmButtonTextColor: confirmButtonTextColor, + cancelButtonColor: cancelButtonColor, + cancelButtonTextColor: cancelButtonTextColor, + avatar: avatar, + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + Color effectiveIconColor = iconColor ?? (isDestructive ? MyColors.redColor : MyColors.textMixColor); + Color effectiveIconBg = iconBackgroundColor ?? effectiveIconColor.withOpacity(0.1); + List effectiveConfirmColors = confirmButtonColors ?? (isDestructive ? const [MyColors.redA3Color, MyColors.redColor] : const [MyColors.gradiantEndColor, MyColors.gradiantStartColor]); + + return SafeArea( + top: false, + child: Container( + padding: const EdgeInsets.fromLTRB(20, 12, 20, 20), + decoration: const BoxDecoration(color: Colors.white, borderRadius: BorderRadius.vertical(top: Radius.circular(24))), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Drag handle + Center( + child: Container(width: 44, height: 4.5, margin: const EdgeInsets.only(bottom: 18), decoration: BoxDecoration(color: const Color(0xFFCBD5E1), borderRadius: BorderRadius.circular(2.5))), + ), + + if (icon != null) ...[ + Center( + child: Container( + width: 56, + height: 56, + alignment: Alignment.center, + decoration: BoxDecoration(color: effectiveIconBg, shape: BoxShape.circle), + child: Icon(icon, color: effectiveIconColor, size: 28), + ), + ), + 16.height, + ], + + (title ?? LocaleKeys.confirm.tr()).toText18(isBold: true, isCentered: true, color: MyColors.darkTextColor), + 8.height, + message.toText14(color: MyColors.grey70Color, textAlign: TextAlign.center), + if (avatar != null) ...[16.height, Center(child: avatar)], + 24.height, + + Row( + children: [ + if (showCancelButton) ...[ + Expanded( + child: DefaultButton( + cancelText ?? LocaleKeys.cancel.tr(), + () { + Navigator.pop(context); + onCancel?.call(); + }, + colors: [cancelButtonColor ?? MyColors.lightGreyEAColor, cancelButtonColor ?? MyColors.lightGreyEAColor], + textColor: cancelButtonTextColor ?? MyColors.darkTextColor, + ), + ), + 12.width, + ], + Expanded( + child: DefaultButton( + confirmText ?? LocaleKeys.ok.tr(), + () { + Navigator.pop(context); + onConfirm(); + }, + colors: effectiveConfirmColors, + textColor: confirmButtonTextColor ?? Colors.white, + ), + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/lib/widgets/button/default_button.dart b/lib/widgets/button/default_button.dart index ba15fad..d355a1f 100644 --- a/lib/widgets/button/default_button.dart +++ b/lib/widgets/button/default_button.dart @@ -17,11 +17,15 @@ class DefaultButton extends StatelessWidget { final Color? color; final Color? disabledColor; final IconData? iconData; + final IconData? trailingIconData; final String? svgIcon; final double? fontSize; final bool isTextExpanded; final int count; final List? colors; + final BoxBorder? border; + final double? borderRadius; + final double? height; const DefaultButton( this.text, @@ -34,8 +38,12 @@ class DefaultButton extends StatelessWidget { this.count = 0, this.textColor = Colors.white, this.iconData, + this.trailingIconData, this.fontSize, this.colors, + this.border, + this.borderRadius, + this.height, }) : super(key: key); @override @@ -43,9 +51,10 @@ class DefaultButton extends StatelessWidget { return InkWell( onTap: onPress, child: Container( - height: 43, + height: height ?? 43, decoration: BoxDecoration( - borderRadius: BorderRadius.circular(6.0), + border: border, + borderRadius: BorderRadius.circular(borderRadius ?? 6.0), gradient: onPress == null ? LinearGradient( colors: [ @@ -67,12 +76,12 @@ class DefaultButton extends StatelessWidget { child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - if (iconData != null) Icon(iconData, color: textColor), + if (iconData != null) Icon(iconData, color: textColor, size: 20), if (svgIcon != null) SvgPicture.asset(svgIcon ?? "", color: textColor), if (!isTextExpanded) Padding( - padding: EdgeInsets.only( - left: (iconData ?? svgIcon) != null ? 6 : 0, + padding: EdgeInsets.symmetric( + horizontal: (iconData ?? svgIcon ?? trailingIconData) != null ? 8 : 0, ), child: Text( text, @@ -98,6 +107,7 @@ class DefaultButton extends StatelessWidget { ), ), ), + if (trailingIconData != null) Icon(trailingIconData, color: textColor, size: 20), if (count > 0) Align( alignment: Alignment.topCenter, diff --git a/lib/widgets/dialogs/confirm_dialog.dart b/lib/widgets/dialogs/confirm_dialog.dart index 560bd26..30a9bef 100644 --- a/lib/widgets/dialogs/confirm_dialog.dart +++ b/lib/widgets/dialogs/confirm_dialog.dart @@ -44,7 +44,7 @@ class ConfirmDialog extends StatelessWidget { constraints: const BoxConstraints(), onPressed: () => onCloseTap ?? Navigator.pop(context), // onPressed: () => Navigator.pop(context), - ) + ), ], ), message.toText16(color: MyColors.lightGrayColor), diff --git a/lib/widgets/dynamic_forms/dynamic_textfield_widget.dart b/lib/widgets/dynamic_forms/dynamic_textfield_widget.dart index 0630e44..f03a833 100644 --- a/lib/widgets/dynamic_forms/dynamic_textfield_widget.dart +++ b/lib/widgets/dynamic_forms/dynamic_textfield_widget.dart @@ -1,11 +1,11 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:mohem_flutter_app/classes/colors.dart'; class DynamicTextFieldWidget extends StatelessWidget { final String labelText; final String hintText; - - // final TextEditingController controller; + final TextEditingController? controller; final VoidCallback? onTap; final IconData? suffixIconData; final bool isEnable; @@ -18,9 +18,22 @@ class DynamicTextFieldWidget extends StatelessWidget { final bool isObscureText; final bool isBackgroundEnable; final void Function(String)? onChange; + final int? maxLength; + final List? inputFormatters; + final Color? backgroundColor; + final Color? borderColor; + final double? borderWidth; + final Color? labelColor; + final Color? textColor; + final Color? suffixIconColor; + final BorderRadius? borderRadius; + final List? boxShadow; + final BoxConstraints? constraints; + final EdgeInsetsGeometry? padding; - DynamicTextFieldWidget(this.labelText, this.hintText, //this.controller, - {this.isObscureText = false, + DynamicTextFieldWidget(this.labelText, this.hintText, + {this.controller, + this.isObscureText = false, this.onTap, this.suffixIconData, this.isEnable = true, @@ -31,20 +44,40 @@ class DynamicTextFieldWidget extends StatelessWidget { this.onChange, this.isInputTypeNum = false, this.isInputTypeNumSigned = true, - this.isBackgroundEnable = false}); + this.isBackgroundEnable = false, + this.maxLength, + this.inputFormatters, + this.backgroundColor, + this.borderColor, + this.borderWidth, + this.labelColor, + this.textColor, + this.suffixIconColor, + this.borderRadius, + this.boxShadow, + this.constraints, + this.padding}); @override Widget build(BuildContext context) { + Color effectiveBorderColor = borderColor ?? MyColors.lightGreyEFColor; + double effectiveBorderWidth = borderWidth ?? 1.0; + Color effectiveLabelColor = labelColor ?? const Color(0xff2B353E); + Color effectiveTextColor = textColor ?? const Color(0xff2B353E); + Color effectiveSuffixColor = suffixIconColor ?? MyColors.darkIconColor; + return Container( - padding: const EdgeInsets.only(left: 16, right: 16, bottom: 15, top: 15), + constraints: constraints, + padding: padding ?? const EdgeInsets.only(left: 16, right: 16, bottom: 15, top: 15), alignment: Alignment.center, decoration: BoxDecoration( - borderRadius: BorderRadius.circular(15), - color: isReadOnly ? MyColors.lightGreyEFColor : Colors.white, + borderRadius: borderRadius ?? BorderRadius.circular(15), + color: backgroundColor ?? (isReadOnly ? MyColors.lightGreyEFColor : Colors.white), border: Border.all( - color: MyColors.lightGreyEFColor, - width: 1, + color: effectiveBorderColor, + width: effectiveBorderWidth, ), + boxShadow: boxShadow, ), child: InkWell( onTap: onTap, @@ -58,14 +91,15 @@ class DynamicTextFieldWidget extends StatelessWidget { if (labelText.isNotEmpty) Text( labelText, - style: const TextStyle( + style: TextStyle( fontSize: 11, fontWeight: FontWeight.w600, - color: Color(0xff2B353E), + color: effectiveLabelColor, letterSpacing: -0.44, ), ), TextField( + controller: controller, enabled: isEnable, scrollPadding: EdgeInsets.zero, readOnly: isReadOnly, @@ -73,22 +107,24 @@ class DynamicTextFieldWidget extends StatelessWidget { ? (isInputTypeNumSigned ? const TextInputType.numberWithOptions(signed: true, decimal: true) : TextInputType.numberWithOptions(signed: true, decimal: true)) : TextInputType.text, textInputAction: TextInputAction.done, - //controller: controller, maxLines: lines, + maxLength: maxLength, + inputFormatters: inputFormatters, obscuringCharacter: "*", obscureText: isObscureText, onChanged: onChange, - style: const TextStyle( + style: TextStyle( fontSize: 14, height: 21 / 14, fontWeight: FontWeight.w400, - color: Color(0xff2B353E), + color: effectiveTextColor, letterSpacing: -0.44, ), decoration: InputDecoration( + counterText: "", isDense: true, hintText: hintText, - fillColor: isReadOnly ? MyColors.borderColor : null, + fillColor: backgroundColor ?? (isReadOnly ? MyColors.borderColor : null), hintStyle: const TextStyle( fontSize: 14, height: 21 / 14, @@ -96,9 +132,6 @@ class DynamicTextFieldWidget extends StatelessWidget { color: Color(0xff575757), letterSpacing: -0.56, ), - // suffixIconConstraints: const BoxConstraints(minWidth: 50), - // suffixIcon: suffixIconData == null ? null : Icon(suffixIconData, color: MyColors.darkTextColor), - // suffixIcon: suffixTap == null ? null : IconButton(icon: Icon(suffixIconData, color: MyColors.darkTextColor), onPressed: suffixTap), contentPadding: EdgeInsets.zero, border: InputBorder.none, focusedBorder: InputBorder.none, @@ -108,8 +141,12 @@ class DynamicTextFieldWidget extends StatelessWidget { ], ), ), - if (isPopup) const Icon(Icons.keyboard_arrow_down_outlined, color: MyColors.darkIconColor), - if (onTap != null) Icon(suffixIconData ?? Icons.keyboard_arrow_down_outlined, color: MyColors.darkIconColor), + if (isPopup) + Icon(Icons.keyboard_arrow_down_outlined, color: effectiveSuffixColor) + else if (onTap != null) + Icon(suffixIconData ?? Icons.keyboard_arrow_down_outlined, color: effectiveSuffixColor) + else if (suffixIconData != null) + Icon(suffixIconData, color: effectiveSuffixColor), ], ), ), diff --git a/lib/widgets/info_chip_widget.dart b/lib/widgets/info_chip_widget.dart new file mode 100644 index 0000000..67bd374 --- /dev/null +++ b/lib/widgets/info_chip_widget.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:mohem_flutter_app/extensions/string_extensions.dart'; + +/// Small pill-shaped badge used for retailer/offer meta info (type, validity, location, usage, ...). +/// Flexible via optional params - every call only needs to override what differs from the given defaults. +class InfoChipWidget extends StatelessWidget { + final String text; + final Color backgroundColor; + final Color textColor; + final Color? iconColor; + final IconData? icon; + final String? svgIcon; + final bool isBold; + final double iconSize; + final double iconTextGap; + final double horizontalPadding; + final double verticalPadding; + final double borderRadius; + final Color? borderColor; + final double borderWidth; + + const InfoChipWidget({ + Key? key, + required this.text, + required this.backgroundColor, + required this.textColor, + this.iconColor, + this.icon, + this.svgIcon, + this.isBold = true, + this.iconSize = 10, + this.iconTextGap = 3, + this.horizontalPadding = 7, + this.verticalPadding = 2.5, + this.borderRadius = 10, + this.borderColor, + this.borderWidth = 1, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.symmetric(horizontal: horizontalPadding, vertical: verticalPadding), + decoration: BoxDecoration( + color: backgroundColor, + borderRadius: BorderRadius.circular(borderRadius), + border: Border.all(color: (borderColor ?? textColor).withValues(alpha: 0.7), width: borderWidth), + ), + child: + (icon == null && svgIcon == null) + ? text.toText10(isBold: isBold, color: textColor) + : Row( + mainAxisSize: MainAxisSize.min, + children: [_buildIcon(), SizedBox(width: iconTextGap), text.toText10(isBold: isBold, color: textColor)], + ), + ); + } + + Widget _buildIcon() { + if (svgIcon != null) { + return SvgPicture.asset(svgIcon!, width: iconSize, height: iconSize, colorFilter: ColorFilter.mode(iconColor ?? textColor, BlendMode.srcIn)); + } + return Icon(icon, size: iconSize, color: iconColor ?? textColor); + } +} diff --git a/lib/widgets/input_widget.dart b/lib/widgets/input_widget.dart index 5059733..67eefae 100644 --- a/lib/widgets/input_widget.dart +++ b/lib/widgets/input_widget.dart @@ -65,10 +65,7 @@ class _InputWidgetState extends State { decoration: BoxDecoration( borderRadius: BorderRadius.circular(15), color: widget.isBackgroundEnable ? Color(0xffF7F7F7) : Colors.white, - border: Border.all( - color: widget.isEnableBorder ? Color(0xffefefef) : Colors.transparent, - width: 1, - ), + border: Border.all(color: widget.isEnableBorder ? Color(0xffefefef) : Colors.transparent, width: 1), ), child: InkWell( onTap: widget.hasSelection ? () {} : null, @@ -79,15 +76,7 @@ class _InputWidgetState extends State { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - widget.labelText, - style: const TextStyle( - fontSize: 11, - fontWeight: FontWeight.w600, - color: Color(0xff2B353E), - letterSpacing: -0.44, - ), - ), + Text(widget.labelText, style: const TextStyle(fontSize: 11, fontWeight: FontWeight.w600, color: Color(0xff2B353E), letterSpacing: -0.44)), TextField( enabled: widget.isEnable, scrollPadding: EdgeInsets.zero, @@ -97,23 +86,11 @@ class _InputWidgetState extends State { obscuringCharacter: "*", obscureText: isObscureText, onChanged: widget.onChange, - style: const TextStyle( - fontSize: 14, - height: 21 / 14, - fontWeight: FontWeight.w400, - color: Color(0xff2B353E), - letterSpacing: -0.44, - ), + style: const TextStyle(fontSize: 14, height: 21 / 14, fontWeight: FontWeight.w400, color: Color(0xff2B353E), letterSpacing: -0.44), decoration: InputDecoration( isDense: true, hintText: widget.hintText, - hintStyle: const TextStyle( - fontSize: 14, - height: 21 / 14, - fontWeight: FontWeight.w400, - color: Color(0xff575757), - letterSpacing: -0.56, - ), + hintStyle: const TextStyle(fontSize: 14, height: 21 / 14, fontWeight: FontWeight.w400, color: Color(0xff575757), letterSpacing: -0.56), suffixIconConstraints: const BoxConstraints(minWidth: 50), suffixIcon: widget.suffixTap == null ? null : IconButton(icon: const Icon(Icons.mic, color: MyColors.darkTextColor), onPressed: widget.suffixTap), contentPadding: EdgeInsets.zero, @@ -131,7 +108,7 @@ class _InputWidgetState extends State { setState(() { isObscureText = !isObscureText; }); - }) + }), ], if (widget.hasSelection) Icon(Icons.keyboard_arrow_down_outlined), ], diff --git a/lib/widgets/mark_attendance_widget.dart b/lib/widgets/mark_attendance_widget.dart index ee258e1..7c44038 100644 --- a/lib/widgets/mark_attendance_widget.dart +++ b/lib/widgets/mark_attendance_widget.dart @@ -17,7 +17,7 @@ import 'package:mohem_flutter_app/models/generic_response_model.dart'; import 'package:mohem_flutter_app/models/privilege_list_model.dart'; import 'package:mohem_flutter_app/provider/dashboard_provider_model.dart'; import 'package:mohem_flutter_app/ui/dialogs/success_dialog.dart'; -import 'package:mohem_flutter_app/widgets/dialogs/confirm_dialog.dart'; +import 'package:mohem_flutter_app/widgets/bottom_sheets/confirm_bottom_sheet.dart'; import 'package:mohem_flutter_app/widgets/dialogs/dialogs.dart'; import 'package:mohem_flutter_app/widgets/location/Location.dart' as location; import 'package:mohem_flutter_app/widgets/nfc/nfc_reader_sheet.dart'; @@ -83,28 +83,19 @@ class _MarkAttendanceWidgetState extends State { if (has) { getHuaweiCurrentLocation(attendanceType); } else { - showDialog( - context: context, - builder: (BuildContext cxt) => ConfirmDialog( - message: "You need to give location permission to mark attendance", - onTap: () { - Navigator.pop(context); - }, - ), - ); + Utils.showToast(LocaleKeys.locationPermissionRequiredForAttendance.tr()); } } }); } else { - showDialog( - context: context, - builder: (BuildContext cxt) => ConfirmDialog( - message: "You need to enable location services to mark attendance", - onTap: () async { - Navigator.pop(context); - await Geolocator.openLocationSettings(); - }, - ), + ConfirmBottomSheet.show( + context, + message: LocaleKeys.locationServicesRequiredForAttendance.tr(), + confirmText: LocaleKeys.openSettings.tr(), + showCancelButton: false, + onConfirm: () async { + await Geolocator.openLocationSettings(); + }, ); } }); @@ -119,7 +110,7 @@ class _MarkAttendanceWidgetState extends State { // showDialog( // context: context, // builder: (BuildContext cxt) => ConfirmDialog( - // message: "You need to give location permission to mark attendance", + // message: LocaleKeys.locationPermissionRequiredForAttendance.tr(), // onTap: () { // Navigator.pop(context); // }, @@ -188,28 +179,26 @@ class _MarkAttendanceWidgetState extends State { context, ); } else { - showDialog( - context: context, - builder: (BuildContext cxt) => ConfirmDialog( - message: "You need to give location permission to mark attendance", - onTap: () async { - Navigator.pop(context); - await Geolocator.openAppSettings(); - }, - ), + ConfirmBottomSheet.show( + context, + message: LocaleKeys.locationPermissionRequiredForAttendance.tr(), + confirmText: LocaleKeys.openSettings.tr(), + showCancelButton: false, + onConfirm: () async { + await Geolocator.openAppSettings(); + }, ); } }); } else { - showDialog( - context: context, - builder: (BuildContext cxt) => ConfirmDialog( - message: "You need to enable location services to mark attendance", - onTap: () async { - Navigator.pop(context); - await Geolocator.openLocationSettings(); - }, - ), + ConfirmBottomSheet.show( + context, + message: LocaleKeys.locationServicesRequiredForAttendance.tr(), + confirmText: LocaleKeys.openSettings.tr(), + showCancelButton: false, + onConfirm: () async { + await Geolocator.openLocationSettings(); + }, ); } }); @@ -242,28 +231,26 @@ class _MarkAttendanceWidgetState extends State { context, ); } else { - showDialog( - context: context, - builder: (BuildContext cxt) => ConfirmDialog( - message: "You need to give location permission to mark attendance", - onTap: () async { - Navigator.pop(context); - await Geolocator.openAppSettings(); - }, - ), + ConfirmBottomSheet.show( + context, + message: LocaleKeys.locationPermissionRequiredForAttendance.tr(), + confirmText: LocaleKeys.openSettings.tr(), + showCancelButton: false, + onConfirm: () async { + await Geolocator.openAppSettings(); + }, ); } }); } else { - showDialog( - context: context, - builder: (BuildContext cxt) => ConfirmDialog( - message: "You need to enable location services to mark attendance", - onTap: () async { - Navigator.pop(context); - await Geolocator.openLocationSettings(); - }, - ), + ConfirmBottomSheet.show( + context, + message: LocaleKeys.locationServicesRequiredForAttendance.tr(), + confirmText: LocaleKeys.openSettings.tr(), + showCancelButton: false, + onConfirm: () async { + await Geolocator.openLocationSettings(); + }, ); } }); @@ -296,28 +283,26 @@ class _MarkAttendanceWidgetState extends State { context, ); } else { - showDialog( - context: context, - builder: (BuildContext cxt) => ConfirmDialog( - message: "You need to give location permission to mark attendance", - onTap: () async { - Navigator.pop(context); - await Geolocator.openAppSettings(); - }, - ), + ConfirmBottomSheet.show( + context, + message: LocaleKeys.locationPermissionRequiredForAttendance.tr(), + confirmText: LocaleKeys.openSettings.tr(), + showCancelButton: false, + onConfirm: () async { + await Geolocator.openAppSettings(); + }, ); } }); } else { - showDialog( - context: context, - builder: (BuildContext cxt) => ConfirmDialog( - message: "You need to enable location services to mark attendance", - onTap: () async { - Navigator.pop(context); - await Geolocator.openLocationSettings(); - }, - ), + ConfirmBottomSheet.show( + context, + message: LocaleKeys.locationServicesRequiredForAttendance.tr(), + confirmText: LocaleKeys.openSettings.tr(), + showCancelButton: false, + onConfirm: () async { + await Geolocator.openLocationSettings(); + }, ); } }); @@ -410,15 +395,7 @@ class _MarkAttendanceWidgetState extends State { GenericResponseModel? g = await DashboardApiClient().markAttendance(pointType: 2, nfcValue: nfcId, isGpsRequired: isNfcLocationEnabled, lat: lat, long: lng); if (g?.messageStatus != 1) { Utils.hideLoading(context); - showDialog( - context: context, - builder: (BuildContext cxt) => ConfirmDialog( - message: g?.errorEndUserMessage ?? "Unexpected error occurred", - onTap: () { - Navigator.pop(context); - }, - ), - ); + Utils.showToast(g?.errorEndUserMessage ?? "Unexpected error occurred"); } else { bool status = await model.fetchAttendanceTracking(context); if (Platform.isIOS) await Future.delayed(const Duration(seconds: 3)); @@ -442,15 +419,7 @@ class _MarkAttendanceWidgetState extends State { GenericResponseModel? g = await DashboardApiClient().markAttendance(pointType: 2, nfcValue: nfcId ?? "", isGpsRequired: isNfcLocationEnabled, lat: lat, long: lng); if (g?.messageStatus != 1) { Utils.hideLoading(context); - showDialog( - context: context, - builder: (BuildContext cxt) => ConfirmDialog( - message: g?.errorEndUserMessage ?? "Unexpected error occurred", - onTap: () { - Navigator.pop(context); - }, - ), - ); + Utils.showToast(g?.errorEndUserMessage ?? "Unexpected error occurred"); } else { bool status = await model.fetchAttendanceTracking(context); Utils.hideLoading(context); @@ -512,17 +481,7 @@ class _MarkAttendanceWidgetState extends State { Utils.hideLoading(context); await closeWifiRequest(); if (g?.messageStatus == 2) { - showDialog( - barrierDismissible: true, - context: context, - builder: (cxt) => ConfirmDialog( - message: g?.errorEndUserMessage ?? "", - onTap: () { - Navigator.pop(context); - }, - onCloseTap: () {}, - ), - ); + Utils.showToast(g?.errorEndUserMessage ?? ""); } else { showMDialog( context, @@ -566,17 +525,7 @@ class _MarkAttendanceWidgetState extends State { bool status = await model.fetchAttendanceTracking(context); Utils.hideLoading(context); if (g?.messageStatus == 2) { - showDialog( - barrierDismissible: true, - context: context, - builder: (cxt) => ConfirmDialog( - message: g?.errorEndUserMessage ?? "", - onTap: () { - Navigator.pop(context); - }, - onCloseTap: () {}, - ), - ); + Utils.showToast(g?.errorEndUserMessage ?? ""); } else { showMDialog( context, diff --git a/lib/widgets/toast/app_toast.dart b/lib/widgets/toast/app_toast.dart new file mode 100644 index 0000000..0217861 --- /dev/null +++ b/lib/widgets/toast/app_toast.dart @@ -0,0 +1,153 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:mohem_flutter_app/classes/colors.dart'; +import 'package:mohem_flutter_app/config/routes.dart'; + +/// Self-dismissing toast with a visible countdown, replacing the generic "just closes" +/// ConfirmDialog for pure-info messages. Mounts via the root navigator's Overlay, so callers +/// don't need to pass a BuildContext (avoids the stale-context issues the dialog path has). +class AppToast { + static OverlayEntry? _entry; + + // Single place to retune how long an action-performing toast waits before firing its + // onComplete (either naturally, or immediately if the user taps it early - see _dismiss). + static const int actionSeconds = 4; + + // Duration for the plain toast that replaces Utils.showToast's old Fluttertoast call - + // change this single value to retune it everywhere that uses it. + static const int plainSeconds = 3; + + // Shared success/error colors - same combo as dependent_details_screen.dart's + // "Active"/"Access Off" badges. Any direct AppToast.show(...) call (not going through + // Utils.showToast) must pass these explicitly, otherwise it silently falls back to the + // plain dark/white style below regardless of whether it's reporting success or failure. + static const Color errorBgColor = Color(0xFFF1CECE); + static const Color errorTextColor = Color(0xFFC53030); + static const Color successBgColor = Color(0xFFECFDF5); + static const Color successTextColor = Color(0xFF059669); + + static void show(String message, {int seconds = 5, VoidCallback? onComplete, bool showIcon = true, bool showCountdownText = true, Color? bgColor, Color? textColor}) { + OverlayState? overlay = AppRoutes.navigatorKey.currentState?.overlay; + if (overlay == null) return; + + // Only one toast on screen at a time - a new call replaces whatever is currently showing. + // Its onComplete (if any) never fires in that case - _dismiss only acts on the entry + // that's still current. + _entry?.remove(); + _entry = null; + + late OverlayEntry entry; + entry = OverlayEntry( + builder: + (BuildContext context) => _CountdownToast( + message: message, + seconds: seconds, + showIcon: showIcon, + showCountdownText: showCountdownText, + bgColor: bgColor, + textColor: textColor, + onDismiss: () => _dismiss(entry, onComplete), + ), + ); + _entry = entry; + overlay.insert(entry); + } + + // Fires on both the timer completing and an early tap - a tap just means "don't make me + // wait", not "cancel this". Guarded so a toast that's already been replaced never fires its + // (now stale) action. + static void _dismiss(OverlayEntry entry, VoidCallback? onComplete) { + if (_entry == entry) { + entry.remove(); + _entry = null; + onComplete?.call(); + } + } +} + +class _CountdownToast extends StatefulWidget { + final String message; + final int seconds; + final VoidCallback onDismiss; + final bool showIcon; + final bool showCountdownText; + final Color? bgColor; + final Color? textColor; + + const _CountdownToast({required this.message, required this.seconds, required this.onDismiss, this.showIcon = true, this.showCountdownText = true, this.bgColor, this.textColor}); + + @override + State<_CountdownToast> createState() => _CountdownToastState(); +} + +class _CountdownToastState extends State<_CountdownToast> { + late int _remaining = widget.seconds; + Timer? _timer; + + @override + void initState() { + super.initState(); + _timer = Timer.periodic(const Duration(seconds: 1), (Timer t) { + setState(() => _remaining--); + if (_remaining <= 0) { + t.cancel(); + widget.onDismiss(); + } + }); + } + + @override + void dispose() { + _timer?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + Color bgColor = widget.bgColor ?? MyColors.darkTextColor; + Color textColor = widget.textColor ?? Colors.white; + + return Positioned( + left: 16, + right: 16, + top: 24, + child: SafeArea( + bottom: false, + child: Material( + color: Colors.transparent, + child: GestureDetector( + onTap: widget.onDismiss, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: bgColor, + borderRadius: BorderRadius.circular(10), + boxShadow: const [BoxShadow(color: Color(0x33000000), blurRadius: 12, offset: Offset(0, 4))], + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (widget.showIcon) ...[Icon(Icons.error_outline, color: textColor, size: 20), const SizedBox(width: 10)], + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text(widget.message, style: TextStyle(color: textColor, fontSize: 14, height: 1.3, fontWeight: FontWeight.w600)), + if (widget.showCountdownText) ...[ + const SizedBox(height: 4), + Text("Dismissing in ${_remaining}s", style: TextStyle(color: textColor.withOpacity(0.7), fontSize: 12, fontWeight: FontWeight.w500)), + ], + ], + ), + ), + ], + ), + ), + ), + ), + ), + ); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index db54c04..c881151 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -56,7 +56,7 @@ dependencies: logger: ^2.8.0 flutter_countdown_timer: ^4.1.0 -# platform_device_id_plus: ^1.0.7 + # platform_device_id_plus: ^1.0.7 # mobile_device_identifier: ^0.0.3 device_info_plus: ^13.2.0 image_picker: ^1.1.2 @@ -64,7 +64,7 @@ dependencies: geolocator: ^14.1.1 month_year_picker: ^0.5.0+1 month_picker_dialog: ^6.0.3 -# open_file: ^3.2.1 + # open_file: ^3.2.1 open_filex: ^4.6.0 wifi_iot: ^0.4.0 # flutter_html: ^3.0.0-alpha.6 @@ -75,10 +75,10 @@ dependencies: # mobile@1234 mobile_scanner: ^7.4.2 # flutter_barcode_scanner: ^2.0.0 -# qr_code_scanner: ^1.0.1 + # qr_code_scanner: ^1.0.1 # qr_flutter: ^4.0.0 url_launcher: ^6.3.3 -# share_plus: ^13.3.0 + share_plus: ^13.3.0 flutter_rating_bar: ^4.0.1 auto_size_text: ^3.0.0 pull_to_refresh: ^2.0.0 @@ -98,7 +98,7 @@ dependencies: logging: ^1.0.1 swipe_to: ^1.0.2 flutter_webrtc: ^1.6.2+hotfix.3 -# camera: ^0.10.3 + # camera: ^0.10.3 camera: ^0.12.1 flutter_local_notifications: ^22.3.1 #firebase_analytics: any @@ -118,9 +118,9 @@ dependencies: #Huawei Dependencies # huawei_hmsavailability: ^6.6.0+300 -# huawei_location: 6.0.0+302 + # huawei_location: 6.0.0+302 huawei_location: ^6.17.0+300 -# huawei_push: ^6.7.0+300 + # huawei_push: ^6.7.0+300 # huawei_map: # git: @@ -149,7 +149,7 @@ dependencies: nfc_manager: ^4.2.1 speech_to_text: ^7.3.0 - + sms_otp_auto_verify: ^2.2.0 # saf: ^1.0.3+4 @@ -192,6 +192,7 @@ flutter: - assets/lottie/ - assets/audio/ - assets/json/ + - assets/pdf/ - assets/images/ - assets/images/worklist/ - assets/images/services_icons/