Compare commits

...

11 Commits

Author SHA1 Message Date
Rand Alshammari f76b204c2d merged 1 month ago
Rand Alshammari 3ceb98c045 merged 2 months ago
Rand Alshammari 1bbb706d8c Merge branch 'faiz_development_common' into fatima_dev
# Conflicts:
#	lib/views/advertisement/components/ads_list_widget.dart
2 months ago
Faiz Hashmi 3146a9433b Completed the Enhancement CR 2 months ago
Rand Alshammari 87ec0f2966 fixed A & E 2 months ago
Faiz Hashmi ff10b66b14 fixing jira bugs 2 months ago
FaizHashmiCS22 388e5b4aed Merge pull request 'fatima_dev' (#5) from fatima_dev into faiz_development_common
Reviewed-on: #5
2 months ago
Rand Alshammari 13966f60a9 Merge remote-tracking branch 'origin/faiz_development_common' into fatima_dev
# Conflicts:
#	lib/views/advertisement/select_ad_type_view.dart
2 months ago
Rand Alshammari 38bda8e8ed fixed A & E 2 months ago
Faiz Hashmi b88aee4632 fixing jira bugs 2 months ago
Fatimah.Alshammari c893cd73ec fix localization and UI updates 3 months ago

@ -1,63 +0,0 @@
# View-Only Mode: CustomerID Parameter Exclusion
## Summary
Updated all API calls to exclude `customerID` parameter when `AppState().getIsViewOnly` is `true`.
## Files Modified
### 1. **request_repo.dart**
- ✅ `createRequest()` - Excludes customerID in view-only mode
- ✅ `getRequests()` - Excludes customerID for customer app type in view-only mode
- ✅ `getRequestBasedOnFilters()` - Excludes customerID for customer app type in view-only mode
- ✅ `getOffersFromProvidersByRequest()` - Excludes customerID in view-only mode
### 2. **ads_repo.dart**
- ✅ `cancelMyAdReservation()` - Excludes customerID in view-only mode
- ✅ `createReserveAd()` - Excludes customerID in view-only mode
- ✅ `uploadBankReceiptsOnReserveDealDone()` - Excludes customerID in view-only mode
### 3. **appointment_repo.dart**
- ✅ `createAppointmentsMulti()` - Excludes customerID in view-only mode
- ✅ `getMyAppointmentsForCustomersByFilters()` - Excludes customerID in view-only mode
### 4. **branch_repo.dart**
- ✅ `submitBranchRatings()` - Excludes customerID in view-only mode
- ✅ `addProviderToFavourite()` - Excludes customerID in view-only mode
- ✅ `getMyFavoriteProviders()` - Excludes customerID in view-only mode
### 5. **setting_options_repo.dart**
- ✅ `appInvitationCreate()` - Excludes customerID for customer app type in view-only mode
## How It Works
When `AppState().getIsViewOnly` is `true`, the application is in demonstration/view-only mode. In this mode:
1. **API parameters are conditionally added**: Instead of always including `customerID`, we check the view-only flag first
2. **Pattern used**:
```dart
var params = {
"otherParam": value,
};
if (!appState.getIsViewOnly) {
params["customerID"] = customerId;
}
```
3. **App types considered**: The changes properly handle both customer and provider app types
## Testing Recommendations
1. Test in normal mode (getIsViewOnly = false):
- All APIs should include customerID as before
- All existing functionality should work
2. Test in view-only mode (getIsViewOnly = true):
- APIs should NOT include customerID parameter
- Read-only operations should work
- Write operations should be blocked by view-only mode
## Note
The `createComplainFromProvider()` method in `common_repo.dart` was NOT modified because the `customerID` in that API represents the target customer being complained about, not the authenticated user making the request.

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

@ -132,14 +132,14 @@
"insert_otp_code": "ادخل الرمز",
"type_code": "الرجاء كتابة الرمز الذي أرسلناه إلى",
"resend_code": "أعد إرسال الرمز",
"check_code": "التحقق من الشفرة",
"check_code": "التحقق من الرمز",
"time_will_expire": "إعادة إرسال الرمز بتنسيق",
"sec": "ثانية",
"providers": "الموفرون",
"appointments": "تعيينات",
"appointments": "المواعيد",
"home": "بيت",
"ads": "إعلانات",
"settings": "إعدادات",
"settings": "الإعدادات",
"view_all": "مشاهدة الكل",
"my_active_Ads": "إعلاناتي النشطة",
"recommended_ads": "الإعلانات الموصى بها",
@ -212,8 +212,8 @@
"noItemSelectedYet": "لم يتم تحديد أي عنصر بعد",
"totalAdditionalAmount": "إجمالي المبلغ الإضافي",
"sar": "ريال سعودي",
"cancel": "يلغي",
"add": "يضيف",
"cancel": "الغاء",
"add": "اضافه",
"itemsAddedSuccessfully": "تمت إضافة العناصر بنجاح",
"pleaseSelectItems": "يرجى اختيار العناصر",
"totalAmount": "المبلغ الإجمالي",
@ -226,10 +226,10 @@
"typeHere": "اكتب هنا...",
"pleaseSelectReason": "يرجى تحديد السبب",
"noAppointmentFound": "لم يتم العثور على مواعيد",
"slotsOverview": "نظرة عامة على الفتحات",
"slotsOverview": "نظرة عامة على الحجوزات",
"empty": "فارغ",
"occupied": "محجوز",
"totalSlots": "إجمالي الفتحات",
"totalSlots": "إجمالي الحجوزات",
"selectAppointments": "اختيار المواعيد",
"appointmentMergeSuccessfully": "دمج المواعيد تم بنجاح",
"updateAppointment": "تحديث الموعد",
@ -238,11 +238,11 @@
"appointmentisCompleted": "تم اكتمال الموعد",
"workStart": "بدء العمل",
"payNow": "ادفع الآن",
"arrived": "وصل",
"arrived": "تم الوصول",
"payLater": "ادفع لاحقًا",
"complete": "اكتمل",
"makeAnOffer": "قدم عرضًا",
"provideOffer": "تقديم العرض",
"provideOffer": "تقديم عرض",
"enterAmount": "أدخل المبلغ",
"submit": "إرسال",
"viewChat": "عرض الدردشة",
@ -254,6 +254,9 @@
"sendOTP": "إرسال رمز OTP",
"noUserAddedYet": "لم يتم إضافة مستخدم بعد",
"assignDealerUser": "تعيين مستخدم تاجر",
"confirmAssignDealerUser": "هل أنت متأكد من رغبتك في تعيين مستخدم التاجر هذا للفرع؟",
"removeDealerUser": "إزالة مستخدم تاجر",
"confirmRemoveDealerUser": "هل أنت متأكد من رغبتك في إزالة مستخدم التاجر هذا من الفرع؟",
"createNewUser": "إنشاء مستخدم جديد",
"dealerUser": "مستخدم تاجر",
"setorEditBranchSchedule": "تعيين أو تحرير جدول الفروع",
@ -406,7 +409,7 @@
"passwordIsStrong": "كلمة المرور الخاصة بك قوية",
"passwordVeryStrong": "كلمة المرور الخاصة بك قوية جدًا",
"selectDuration": "اختر المدة",
"daysVar": "الأيام",
"daysVar": "يوم",
"adCharges": "رسوم الإعلان",
"select": "اختر",
"deleteAdConfirmation": "هل تريد حذف الإعلان؟",
@ -417,8 +420,12 @@
"accidentFree": "خالية من الحوادث",
"brandNew": "جديدة",
"used": "مستعملة",
"all": "الكل",
"newSparePart": "قطع غيار جديدة",
"usedSparePart": "قطع غيار مستعملة",
"hasYourCarHadAccident": "هل تعرضت سيارتك لأي حادث؟",
"odometerReading": "قراءة عداد المسافات (كم)",
"canMileageIncrease": "هل يمكن زيادة المسافة المقطوعة عند الفحص؟",
"currentlyInUse": "قيد الاستخدام حاليًا",
"demand": "الطلب",
"adDurationExpired": "انتهت مدة عرض الإعلان الخاص بك",
@ -590,6 +597,9 @@
"myNearbyBranches": "فروعي القريبة",
"myRecentBranches": "فروعي الأخيرة",
"favoriteList": "قائمة المفضلة",
"myFavouriteProviders": "مزودي الخدمة المفضلين",
"myFavouriteAds": "إعلاناتي المفضلة",
"noFavouriteAds": "لا توجد إعلانات مفضلة",
"noReviewsBranch": "هذا الفرع لا يوجد لديه تعليقات.",
"readLess": "اقرأ أقل",
"readMoreReviews": "اقرأ المزيد من التقييمات",
@ -655,7 +665,7 @@
"servicesNotAvailableHomeLocation": "بعض الخدمات غير متوفرة في الموقع المنزلي.",
"additionalChargesNotice": "هذه الرسوم إضافية على رسوم الخدمة الفعلية. قد تختلف الرسوم للعناصر الثقيلة.",
"selectServiceLocation": "اختر موقع الخدمة",
"apptOn": "التعيين في",
"apptOn": "الموعد في",
"merged": "مُدمج",
"date": "التاريخ",
"selectAll": "اختر الكل",
@ -817,13 +827,80 @@
"specialRequestsChats": "دردشات الطلبات الخاصة",
"selectDeliveryOption": "اختر خيار التوصيل",
"deliveryOptions": "خيارات التوصيل",
"selfPickup": "الالتقاط الذاتي",
"selfPickup": "استلام ذاتي",
"updateYourLocationInfo": "قم بتحديث معلومات موقعك",
"initiateSelfPickup": "بدء الاستلام الذاتي",
"selfPickupStatus": "حالة الالتقاط الذاتي",
"selfPickupStatus": "حالة الاستلام الذاتي",
"myDraftAds": "مسودتي للإعلانات",
"scheduleDeletedSuccessfully": "تم حذف الجدول بنجاح",
"addValidAddress": "رجى إضافة عنوان صالح",
"termsPrivacyPolicy": "الشروط وسياسة الخصوصية"
"termsPrivacyPolicy": "الشروط وسياسة الخصوصية",
"pendingApproval": "قيد الموافقة",
"inReview": "قيد المراجعة",
"rejected": "مرفوض",
"deactivated": "معطل",
"blocked": "محظور",
"allRequests": "جميع الطلبات",
"pending": "قيد الانتظار",
"preparingToCollect": "جاري التحضير للاستلام",
"readyToCollect": "جاهز للاستلام",
"collected": "تم الاستلام",
"initiated": "تم البدء",
"inTransit": "قيد النقل",
"outForDelivery": "قيد التسليم",
"delivered": "تم التسليم",
"negotiate": "تفاوض",
"accepted": "مقبول",
"cancelled": "ملغى",
"loginToAnotherAccount": "تسجيل الدخول إلى حساب آخر",
"silver": "فضي",
"phoneWithoutCountryCode": "رقم الهاتف بدون رمز الدوله",
"sunday": "الأحد",
"monday": "الإثنين",
"tuesday": "الثلاثاء",
"wednesday": "الأربعاء",
"thursday": "الخميس",
"friday": "الجمعة",
"saturday": "السبت",
"adsWith": "الإعلانات:",
"usersWith": "المستخدمون:",
"branchesWith": "الفروع:",
"subscribed": "مشترك",
"expires": "تنتهي",
"renew": "تجديد",
"km": "كم",
"items": "عناصر",
"no_items": "لا توجد عناصر",
"allAds": "جميع الاعلانات",
"reserveCancel": "الغاء الحجز",
"pendingForReview": "قيد المراجعة",
"pendingForPayment": "قيد الدفع",
"rejected_n": "مرفوض",
"canceled": "ملغى",
"pendingForPost": "قيد النشر",
"active_n": "نشط",
"expired_n": "منتهي الصلاحية",
"sold_n": "مباع",
"reserved_n": "محجوز",
"buyingService": "خدمة الشراء",
"deActive": "غير نشط",
"pendingForActive": "قيد التفعيل",
"updateItem": "تحديث العنصر",
"createItem": "إنشاء عنصر",
"pleaseAddAtLeastOneItemImage": "يرجى إضافة صورة واحدة على الأقل للعنصر",
"pleaseAddValidItemName": "يرجى إضافة اسم عنصر صحيح",
"pleaseAddValidItemDescription": "يرجى إضافة وصف عنصر صحيح",
"pleaseAddValidItemPrice": "يرجى إضافة سعر عنصر صحيح",
"remaining": "متبقي",
"month": "شهر",
"currentPackage": "الحزمة الحالية",
"forVar": "لمدة",
"branch": "الفرع",
"shareAd": "مشاركة الإعلان",
"checkOutThisAd": "تفقد هذه السيارة الرائعة!",
"downloadApp": "تحميل تطبيق موّاتر:",
"androidApp": "أندرويد",
"iosApp": "آيفون",
"pleaseWait": "يرجى الانتظار..."
}

@ -254,6 +254,9 @@
"sendOTP": "Send OTP",
"noUserAddedYet": "No User Added Yet",
"assignDealerUser": "Assign Dealer User",
"confirmAssignDealerUser": "Are you sure you want to assign this dealer user to the branch?",
"removeDealerUser": "Remove Dealer User",
"confirmRemoveDealerUser": "Are you sure you want to remove this dealer user from the branch?",
"createNewUser": "Create New User",
"dealerUser": "Dealer User",
"setorEditBranchSchedule": "Set or Edit Branch Schedule",
@ -418,8 +421,12 @@
"accidentFree": "Accident Free",
"brandNew": "Brand New",
"used": "Used",
"all": "All",
"newSparePart": "New Spare Part",
"usedSparePart": "Used Spare Part",
"hasYourCarHadAccident": "Has your car had any accident?",
"odometerReading": "Odometer Reading (Km)",
"canMileageIncrease": "Can the mileage be increased when checking?",
"currentlyInUse": "Currently in Use",
"demand": "Demand",
"adDurationExpired": "Your Ad Duration time is over",
@ -591,6 +598,9 @@
"myNearbyBranches": "My Nearby Branches",
"myRecentBranches": "My Recent Branches",
"favoriteList": "Favorite List",
"myFavouriteProviders": "My Favourite Providers",
"myFavouriteAds": "My Favourite Ads",
"noFavouriteAds": "No Favourite Ads Found",
"noReviewsBranch": "This branch has no reviews.",
"readLess": "Read Less",
"readMoreReviews": "Read More Reviews",
@ -822,5 +832,72 @@
"myDraftAds": "My Draft Ads",
"scheduleDeletedSuccessfully": "The schedule has been deleted successfully",
"addValidAddress": "Please add a valid address",
"termsPrivacyPolicy": "Terms & Privacy Policy"
"termsPrivacyPolicy": "Terms & Privacy Policy",
"pendingApproval": "Pending Approval",
"inReview": "In Review",
"rejected": "Rejected",
"deactivated": "Deactivated",
"blocked": "Blocked",
"allRequests": "All Requests",
"pending": "Pending",
"preparingToCollect": "Preparing To Collect",
"readyToCollect": "Ready To Collect",
"collected": "Collected",
"initiated": "Initiated",
"inTransit": "In Transit",
"outForDelivery": "Out For De,livery",
"negotiate": "Negotiate",
"accepted": "Accepted",
"cancelled": "Cancelled",
"delivered": "Delivered",
"loginToAnotherAccount": "Login To Another Account",
"silver": "Silver",
"phoneWithoutCountryCode": "Phone without country code",
"sunday": "Sunday",
"monday": "Monday",
"tuesday": "Tuesday",
"wednesday": "Wednesday",
"thursday": "Thursday",
"friday": "Friday",
"saturday": "Saturday",
"adsWith": "Ads:",
"usersWith": "Users:",
"branchesWith": "Branches:",
"subscribed": "Subscribed",
"expires": "Expires",
"renew": "Renew",
"km": "km",
"items": "items",
"no_items": "No items",
"allAds": "All Ads",
"reserveCancel": "Reserve Cancel",
"pendingForReview": "Pending For Review",
"pendingForPayment": "Pending For Payment",
"rejected_n": "Rejected",
"canceled": "Cancelled",
"pendingForPost": "Pending For Post",
"active_n": "Active",
"expired_n": "Expired",
"sold_n": "Sold",
"reserved_n": "Reserved",
"buyingService": "Buying Service",
"deActive": "De Active",
"pendingForActive": "Pending For Active",
"updateItem": "Update Item",
"createItem": "Create Item",
"pleaseAddAtLeastOneItemImage": "Please add at least one item image",
"pleaseAddValidItemName": "Please add valid item name",
"pleaseAddValidItemDescription": "Please add valid item description",
"pleaseAddValidItemPrice": "Please add valid item price",
"remaining": "remaining",
"month": "Month",
"currentPackage": "Current Package",
"forVar": "for",
"branch": "branch",
"shareAd": "Share Ad",
"checkOutThisAd": "Check out this amazing vehicle!",
"downloadApp": "Download MoWater App:",
"androidApp": "Android",
"iosApp": "iOS",
"pleaseWait": "Please wait..."
}

@ -0,0 +1,3 @@
description: This file stores settings for Dart & Flutter DevTools.
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
extensions:

@ -138,7 +138,7 @@ class ApiClientImp implements ApiClient {
logger.i(url);
logger.d("------Payload------");
logger.i(jsonDecode(requestBody));
logger.d("------Response------");
// logger.d("------Response------");
logger.i(jsonDecode(response.body));
}
if (response.statusCode >= 200 && response.statusCode < 500) {
@ -249,7 +249,7 @@ class ApiClientImp implements ApiClient {
logger.i(url);
logger.i("$queryParameters");
}
var response = await _get(Uri.parse(url), headers: headers0).timeout(const Duration(seconds: 60));
var response = await _get(Uri.parse(url), headers: headers0).timeout(const Duration(seconds: 120));
if (!kReleaseMode) {
logger.i(jsonDecode(response.body));

@ -1,3 +1,4 @@
import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:mc_common_app/models/general_models/post_params_model.dart';
import 'package:mc_common_app/models/subscriptions_models/provider_subscription_model.dart';
@ -35,6 +36,9 @@ class AppState {
_deviceType = value;
}
bool isArabic(BuildContext context) =>
Localizations.localeOf(context).languageCode == 'ar';
AppType currentAppType = AppType.provider;
UserType userType = UserType.customer;
@ -66,7 +70,8 @@ class AppState {
List<Subscription>? _providerSubscription;
List<Subscription> get getproviderSubscription => _providerSubscription ?? [Subscription(name: "")];
List<Subscription> get getproviderSubscription =>
_providerSubscription ?? [Subscription(name: "")];
set setproviderSubscription(List<Subscription>? value) {
_providerSubscription = value;
@ -74,7 +79,8 @@ class AppState {
DropValue? _userRegisterCountrySelection;
DropValue get getUserRegisterCountrySelection => _userRegisterCountrySelection!;
DropValue get getUserRegisterCountrySelection =>
_userRegisterCountrySelection!;
set setUserRegisterCountrySelection(DropValue value) {
_userRegisterCountrySelection = value;
@ -87,6 +93,4 @@ class AppState {
set setIsViewOnly(bool value) {
_isViewOnly = value;
}
}

@ -127,6 +127,9 @@ class ApiConsts {
static String vehicleAdsSingleStepUpdate = "${baseUrlServices}api/Advertisement/AdsSingleStep_Update";
static String vehicleAdsSingleStepUpdateExtend = "${baseUrlServices}api/Advertisement/AdsSingleStep_Update_Extend";
static String vehicleAdsGet = "${baseUrlServices}api/Advertisement/Ads_Get";
static String favouriteAdsGet = "${baseUrlServices}api/Advertisement/FavouriteAds_Get";
static String favouriteAdsCreate = "${baseUrlServices}api/Advertisement/FavouriteAds_Create";
static String unFavouriteAds = "${baseUrlServices}api/Advertisement/UnFavourite_Ads";
static String myAdsReserveGet = "${baseUrlServices}api/Advertisement/AdsReserve_Get";
static String reserveAdsBankDetailsGet = "${baseUrlServices}api/Advertisement/MCBankAccountAd_Get";
static String adsUpdateStatus = "${baseUrlServices}api/Advertisement/Ads_UpdateStatus";
@ -181,9 +184,8 @@ class ApiConsts {
static String requestOfferUpdateDeliveryOptionByCustomer = "${baseUrlServices}api/RequestManagement/RequestOffer_UpdateDeliveryByCustomer";
static String requestUpdateAdditionalAddress = "${baseUrlServices}api/RequestManagement/Request_UpdateAdditionalAddress";
//Shipping
static String shippingRequestStatusUpdate = "${baseUrlServices}api/RequestManagement/ShippingRequestStatus_Update";
static String shippingRequestStatusUpdate = "${baseUrlServices}api/RequestManagement/ShippingRequestStatus_Update";
static String shippingRequestStatusGet = "${baseUrlServices}api/RequestManagement/ShippingRequestStatus_Get";
static String selfPickupRequestStatusGet = "${baseUrlServices}api/RequestManagement/SelfPickUpRequestStatus_Get";
static String selfPickupRequestStatusUpdate = "${baseUrlServices}api/RequestManagement/SelfPickUpRequestStatus_Update";
@ -290,6 +292,7 @@ class MyAssets {
//JPEG
static String bnCar = "${assetPath}images/bn_car.jpeg";
static String bnCarFlashingLights = "${assetPath}images/bn_car_flashing_lights.gif";
static String carBanner = "${assetPath}images/bn_car.jpeg";
//SVG
@ -468,3 +471,4 @@ class GuestConsts {
},
);
}

@ -143,7 +143,8 @@ class AppRoutes {
static const String branchDetailView = "/branchDetailPage";
static const String branchSearchFilterPage = "/branchSearchFilterPage";
static const String providerProfileView = "/providerProfilePage";
static const String favoriteListView = "/favoriteListView";
static const String favoriteProvidersView = "/favoriteProvidersView";
static const String favouriteAdsView = "/favouriteAdsView";
//Requests
static const String requestsDetailPage = "/requestsDetailPage";

@ -8,6 +8,8 @@ import 'package:mc_common_app/classes/consts.dart';
import 'package:mc_common_app/theme/colors.dart';
import 'package:mc_common_app/utils/date_helper.dart';
import 'package:mc_common_app/utils/enums.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:mc_common_app/generated/locale_keys.g.dart';
extension EmailValidator on String {
Widget toText(
@ -288,6 +290,8 @@ extension AppointmentStatusToInt on AppointmentStatusEnum {
case AppointmentStatusEnum.upcoming:
return 11;
case AppointmentStatusEnum.served:
return 12;
case AppointmentStatusEnum.allAppointments:
return 0;
@ -384,6 +388,8 @@ extension AppointmentEnum on int {
return AppointmentStatusEnum.past;
} else if (this == 11) {
return AppointmentStatusEnum.upcoming;
} else if (this == 12) {
return AppointmentStatusEnum.served;
} else {
return AppointmentStatusEnum.allAppointments;
}
@ -438,6 +444,8 @@ extension RequestTypeTypEnum on int {
return RequestsTypeEnum.specialCarRequest;
} else if (this == 2) {
return RequestsTypeEnum.serviceRequest;
} else if (this == 3) {
return RequestsTypeEnum.other;
}
return RequestsTypeEnum.specialCarRequest;
}
@ -452,6 +460,9 @@ extension RequestTypeStatusToInt on RequestsTypeEnum {
case RequestsTypeEnum.serviceRequest:
return 2;
case RequestsTypeEnum.other:
return 3;
default:
return 0;
}
@ -675,30 +686,89 @@ extension BranchServiceEnumStatusToInt on ServiceStatusEnum {
}
}
// extension DateTimeConversions on DateTime {
// String getTimeAgo({bool numericDates = true, bool isArabic = false,}) {
// final date2 = DateTime.now();
// final difference = date2.difference(this);
// if ((difference.inDays / 7).floor() >= 2) {
// return DateHelper.formatAsDayMonthYear(this, isArabic: isArabic,);
// } else if ((difference.inDays / 7).floor() >= 1) {
// return (numericDates) ? '1 week ago' : 'Last week';
// } else if (difference.inDays >= 2) {
// return '${difference.inDays} days ago';
// } else if (difference.inDays >= 1) {
// return (numericDates) ? '1 day ago' : 'Yesterday';
// } else if (difference.inHours >= 2) {
// return '${difference.inHours} hours ago';
// } else if (difference.inHours >= 1) {
// return (numericDates) ? '1 hour ago' : 'An hour ago';
// } else if (difference.inMinutes >= 2) {
// return '${difference.inMinutes} min ago';
// } else if (difference.inMinutes >= 1) {
// return (numericDates) ? '1 min ago' : 'A min ago';
// } else if (difference.inSeconds >= 3) {
// return '${difference.inSeconds} sec ago';
// } else {
// return 'Just now';
// }
// }
// }
extension DateTimeConversions on DateTime {
String getTimeAgo({bool numericDates = true}) {
final date2 = DateTime.now();
final difference = date2.difference(this);
String getTimeAgo({
bool numericDates = true,
bool isArabic = false,
}) {
final now = DateTime.now();
final difference = now.difference(this);
if ((difference.inDays / 7).floor() >= 2) {
return DateHelper.formatAsDayMonthYear(this);
return DateHelper.formatAsDayMonthYear(
this,
isArabic: isArabic,
);
} else if ((difference.inDays / 7).floor() >= 1) {
return (numericDates) ? '1 week ago' : 'Last week';
if (isArabic) {
return numericDates ? 'منذ أسبوع' : 'الأسبوع الماضي';
}
return numericDates ? '1 week ago' : 'Last week';
} else if (difference.inDays >= 2) {
return '${difference.inDays} days ago';
return isArabic
? 'منذ ${difference.inDays} أيام'
: '${difference.inDays} days ago';
} else if (difference.inDays >= 1) {
return (numericDates) ? '1 day ago' : 'Yesterday';
if (isArabic) {
return numericDates ? 'منذ يوم' : 'أمس';
}
return numericDates ? '1 day ago' : 'Yesterday';
} else if (difference.inHours >= 2) {
return '${difference.inHours} hours ago';
return isArabic
? 'منذ ${difference.inHours} ساعات'
: '${difference.inHours} hours ago';
} else if (difference.inHours >= 1) {
return (numericDates) ? '1 hour ago' : 'An hour ago';
if (isArabic) {
return numericDates ? 'منذ ساعة' : 'قبل ساعة';
}
return numericDates ? '1 hour ago' : 'An hour ago';
} else if (difference.inMinutes >= 2) {
return '${difference.inMinutes} min ago';
return isArabic
? 'منذ ${difference.inMinutes} دقائق'
: '${difference.inMinutes} min ago';
} else if (difference.inMinutes >= 1) {
return (numericDates) ? '1 min ago' : 'A min ago';
if (isArabic) {
return numericDates ? 'منذ دقيقة' : 'قبل دقيقة';
}
return numericDates ? '1 min ago' : 'A min ago';
} else if (difference.inSeconds >= 3) {
return '${difference.inSeconds} sec ago';
return isArabic
? 'منذ ${difference.inSeconds} ثوانٍ'
: '${difference.inSeconds} sec ago';
} else {
return 'Just now';
return isArabic ? 'الآن' : 'Just now';
}
}
}
@ -1075,13 +1145,13 @@ extension RequestDeliveryOptionEnumTosTRINGExt on RequestDeliveryOptionEnum {
String getStringFromRequestDeliveryOptionEnum() {
switch (this) {
case RequestDeliveryOptionEnum.delivery:
return "Delivery";
return LocaleKeys.delivery.tr();
case RequestDeliveryOptionEnum.selfPickup:
return "Self Pickup";
return LocaleKeys.selfPickup.tr();
case RequestDeliveryOptionEnum.both:
return "Delivery, Self Pickup";
return "${LocaleKeys.delivery.tr()}, ${LocaleKeys.selfPickup.tr()}";
default:
return "Self Pickup";
return LocaleKeys.selfPickup.tr();
}
}
}

@ -148,14 +148,14 @@ class CodegenLoader extends AssetLoader{
"insert_otp_code": "ادخل الرمز",
"type_code": "الرجاء كتابة الرمز الذي أرسلناه إلى",
"resend_code": "أعد إرسال الرمز",
"check_code": "التحقق من الشفرة",
"check_code": "التحقق من الرمز",
"time_will_expire": "إعادة إرسال الرمز بتنسيق",
"sec": "ثانية",
"providers": "الموفرون",
"appointments": "تعيينات",
"appointments": "المواعيد",
"home": "بيت",
"ads": "إعلانات",
"settings": "إعدادات",
"settings": "الإعدادات",
"view_all": "مشاهدة الكل",
"my_active_Ads": "إعلاناتي النشطة",
"recommended_ads": "الإعلانات الموصى بها",
@ -228,8 +228,8 @@ class CodegenLoader extends AssetLoader{
"noItemSelectedYet": "لم يتم تحديد أي عنصر بعد",
"totalAdditionalAmount": "إجمالي المبلغ الإضافي",
"sar": "ريال سعودي",
"cancel": "يلغي",
"add": "يضيف",
"cancel": "الغاء",
"add": "اضافه",
"itemsAddedSuccessfully": "تمت إضافة العناصر بنجاح",
"pleaseSelectItems": "يرجى اختيار العناصر",
"totalAmount": "المبلغ الإجمالي",
@ -242,10 +242,10 @@ class CodegenLoader extends AssetLoader{
"typeHere": "اكتب هنا...",
"pleaseSelectReason": "يرجى تحديد السبب",
"noAppointmentFound": "لم يتم العثور على مواعيد",
"slotsOverview": "نظرة عامة على الفتحات",
"slotsOverview": "نظرة عامة على الحجوزات",
"empty": "فارغ",
"occupied": "محجوز",
"totalSlots": "إجمالي الفتحات",
"totalSlots": "إجمالي الحجوزات",
"selectAppointments": "اختيار المواعيد",
"appointmentMergeSuccessfully": "دمج المواعيد تم بنجاح",
"updateAppointment": "تحديث الموعد",
@ -254,11 +254,11 @@ class CodegenLoader extends AssetLoader{
"appointmentisCompleted": "تم اكتمال الموعد",
"workStart": "بدء العمل",
"payNow": "ادفع الآن",
"arrived": "وصل",
"arrived": "تم الوصول",
"payLater": "ادفع لاحقًا",
"complete": "اكتمل",
"makeAnOffer": "قدم عرضًا",
"provideOffer": "تقديم العرض",
"provideOffer": "تقديم عرض",
"enterAmount": "أدخل المبلغ",
"submit": "إرسال",
"viewChat": "عرض الدردشة",
@ -270,6 +270,9 @@ class CodegenLoader extends AssetLoader{
"sendOTP": "إرسال رمز OTP",
"noUserAddedYet": "لم يتم إضافة مستخدم بعد",
"assignDealerUser": "تعيين مستخدم تاجر",
"confirmAssignDealerUser": "هل أنت متأكد من رغبتك في تعيين مستخدم التاجر هذا للفرع؟",
"removeDealerUser": "إزالة مستخدم تاجر",
"confirmRemoveDealerUser": "هل أنت متأكد من رغبتك في إزالة مستخدم التاجر هذا من الفرع؟",
"createNewUser": "إنشاء مستخدم جديد",
"dealerUser": "مستخدم تاجر",
"setorEditBranchSchedule": "تعيين أو تحرير جدول الفروع",
@ -422,7 +425,7 @@ class CodegenLoader extends AssetLoader{
"passwordIsStrong": "كلمة المرور الخاصة بك قوية",
"passwordVeryStrong": "كلمة المرور الخاصة بك قوية جدًا",
"selectDuration": "اختر المدة",
"daysVar": "الأيام",
"daysVar": "يوم",
"adCharges": "رسوم الإعلان",
"select": "اختر",
"deleteAdConfirmation": "هل تريد حذف الإعلان؟",
@ -433,8 +436,12 @@ class CodegenLoader extends AssetLoader{
"accidentFree": "خالية من الحوادث",
"brandNew": "جديدة",
"used": "مستعملة",
"all": "الكل",
"newSparePart": "قطع غيار جديدة",
"usedSparePart": "قطع غيار مستعملة",
"hasYourCarHadAccident": "هل تعرضت سيارتك لأي حادث؟",
"odometerReading": "قراءة عداد المسافات (كم)",
"canMileageIncrease": "هل يمكن زيادة المسافة المقطوعة عند الفحص؟",
"currentlyInUse": "قيد الاستخدام حاليًا",
"demand": "الطلب",
"adDurationExpired": "انتهت مدة عرض الإعلان الخاص بك",
@ -606,6 +613,9 @@ class CodegenLoader extends AssetLoader{
"myNearbyBranches": "فروعي القريبة",
"myRecentBranches": "فروعي الأخيرة",
"favoriteList": "قائمة المفضلة",
"myFavouriteProviders": "مزودي الخدمة المفضلين",
"myFavouriteAds": "إعلاناتي المفضلة",
"noFavouriteAds": "لا توجد إعلانات مفضلة",
"noReviewsBranch": "هذا الفرع لا يوجد لديه تعليقات.",
"readLess": "اقرأ أقل",
"readMoreReviews": "اقرأ المزيد من التقييمات",
@ -671,7 +681,7 @@ class CodegenLoader extends AssetLoader{
"servicesNotAvailableHomeLocation": "بعض الخدمات غير متوفرة في الموقع المنزلي.",
"additionalChargesNotice": "هذه الرسوم إضافية على رسوم الخدمة الفعلية. قد تختلف الرسوم للعناصر الثقيلة.",
"selectServiceLocation": "اختر موقع الخدمة",
"apptOn": "التعيين في",
"apptOn": "الموعد في",
"merged": "مُدمج",
"date": "التاريخ",
"selectAll": "اختر الكل",
@ -833,14 +843,81 @@ class CodegenLoader extends AssetLoader{
"specialRequestsChats": "دردشات الطلبات الخاصة",
"selectDeliveryOption": "اختر خيار التوصيل",
"deliveryOptions": "خيارات التوصيل",
"selfPickup": "الالتقاط الذاتي",
"selfPickup": "استلام ذاتي",
"updateYourLocationInfo": "قم بتحديث معلومات موقعك",
"initiateSelfPickup": "بدء الاستلام الذاتي",
"selfPickupStatus": "حالة الالتقاط الذاتي",
"selfPickupStatus": "حالة الاستلام الذاتي",
"myDraftAds": "مسودتي للإعلانات",
"scheduleDeletedSuccessfully": "تم حذف الجدول بنجاح",
"addValidAddress": "رجى إضافة عنوان صالح",
"termsPrivacyPolicy": "الشروط وسياسة الخصوصية"
"termsPrivacyPolicy": "الشروط وسياسة الخصوصية",
"pendingApproval": "قيد الموافقة",
"inReview": "قيد المراجعة",
"rejected": "مرفوض",
"deactivated": "معطل",
"blocked": "محظور",
"allRequests": "جميع الطلبات",
"pending": "قيد الانتظار",
"preparingToCollect": "جاري التحضير للاستلام",
"readyToCollect": "جاهز للاستلام",
"collected": "تم الاستلام",
"initiated": "تم البدء",
"inTransit": "قيد النقل",
"outForDelivery": "قيد التسليم",
"delivered": "تم التسليم",
"negotiate": "تفاوض",
"accepted": "مقبول",
"cancelled": "ملغى",
"loginToAnotherAccount": "تسجيل الدخول إلى حساب آخر",
"silver": "فضي",
"phoneWithoutCountryCode": "رقم الهاتف بدون رمز الدوله",
"sunday": "الأحد",
"monday": "الإثنين",
"tuesday": "الثلاثاء",
"wednesday": "الأربعاء",
"thursday": "الخميس",
"friday": "الجمعة",
"saturday": "السبت",
"adsWith": "الإعلانات:",
"usersWith": "المستخدمون:",
"branchesWith": "الفروع:",
"subscribed": "مشترك",
"expires": "تنتهي",
"renew": "تجديد",
"km": "كم",
"items": "عناصر",
"no_items": "لا توجد عناصر",
"allAds": "جميع الاعلانات",
"reserveCancel": "الغاء الحجز",
"pendingForReview": "قيد المراجعة",
"pendingForPayment": "قيد الدفع",
"rejected_n": "مرفوض",
"canceled": "ملغى",
"pendingForPost": "قيد النشر",
"active_n": "نشط",
"expired_n": "منتهي الصلاحية",
"sold_n": "مباع",
"reserved_n": "محجوز",
"buyingService": "خدمة الشراء",
"deActive": "غير نشط",
"pendingForActive": "قيد التفعيل",
"updateItem": "تحديث العنصر",
"createItem": "إنشاء عنصر",
"pleaseAddAtLeastOneItemImage": "يرجى إضافة صورة واحدة على الأقل للعنصر",
"pleaseAddValidItemName": "يرجى إضافة اسم عنصر صحيح",
"pleaseAddValidItemDescription": "يرجى إضافة وصف عنصر صحيح",
"pleaseAddValidItemPrice": "يرجى إضافة سعر عنصر صحيح",
"remaining": "متبقي",
"month": "شهر",
"currentPackage": "الحزمة الحالية",
"forVar": "لمدة",
"branch": "الفرع",
"shareAd": "مشاركة الإعلان",
"checkOutThisAd": "تفقد هذه السيارة الرائعة!",
"downloadApp": "تحميل تطبيق موّاتر:",
"androidApp": "أندرويد",
"iosApp": "آيفون",
"pleaseWait": "يرجى الانتظار..."
};
static const Map<String,dynamic> _en_US = {
"firstTimeLogIn": "First Time Log In",
@ -1098,6 +1175,9 @@ static const Map<String,dynamic> _en_US = {
"sendOTP": "Send OTP",
"noUserAddedYet": "No User Added Yet",
"assignDealerUser": "Assign Dealer User",
"confirmAssignDealerUser": "Are you sure you want to assign this dealer user to the branch?",
"removeDealerUser": "Remove Dealer User",
"confirmRemoveDealerUser": "Are you sure you want to remove this dealer user from the branch?",
"createNewUser": "Create New User",
"dealerUser": "Dealer User",
"setorEditBranchSchedule": "Set or Edit Branch Schedule",
@ -1262,8 +1342,12 @@ static const Map<String,dynamic> _en_US = {
"accidentFree": "Accident Free",
"brandNew": "Brand New",
"used": "Used",
"all": "All",
"newSparePart": "New Spare Part",
"usedSparePart": "Used Spare Part",
"hasYourCarHadAccident": "Has your car had any accident?",
"odometerReading": "Odometer Reading (Km)",
"canMileageIncrease": "Can the mileage be increased when checking?",
"currentlyInUse": "Currently in Use",
"demand": "Demand",
"adDurationExpired": "Your Ad Duration time is over",
@ -1435,6 +1519,9 @@ static const Map<String,dynamic> _en_US = {
"myNearbyBranches": "My Nearby Branches",
"myRecentBranches": "My Recent Branches",
"favoriteList": "Favorite List",
"myFavouriteProviders": "My Favourite Providers",
"myFavouriteAds": "My Favourite Ads",
"noFavouriteAds": "No Favourite Ads Found",
"noReviewsBranch": "This branch has no reviews.",
"readLess": "Read Less",
"readMoreReviews": "Read More Reviews",
@ -1666,7 +1753,74 @@ static const Map<String,dynamic> _en_US = {
"myDraftAds": "My Draft Ads",
"scheduleDeletedSuccessfully": "The schedule has been deleted successfully",
"addValidAddress": "Please add a valid address",
"termsPrivacyPolicy": "Terms & Privacy Policy"
"termsPrivacyPolicy": "Terms & Privacy Policy",
"pendingApproval": "Pending Approval",
"inReview": "In Review",
"rejected": "Rejected",
"deactivated": "Deactivated",
"blocked": "Blocked",
"allRequests": "All Requests",
"pending": "Pending",
"preparingToCollect": "Preparing To Collect",
"readyToCollect": "Ready To Collect",
"collected": "Collected",
"initiated": "Initiated",
"inTransit": "In Transit",
"outForDelivery": "Out For De,livery",
"negotiate": "Negotiate",
"accepted": "Accepted",
"cancelled": "Cancelled",
"delivered": "Delivered",
"loginToAnotherAccount": "Login To Another Account",
"silver": "Silver",
"phoneWithoutCountryCode": "Phone without country code",
"sunday": "Sunday",
"monday": "Monday",
"tuesday": "Tuesday",
"wednesday": "Wednesday",
"thursday": "Thursday",
"friday": "Friday",
"saturday": "Saturday",
"adsWith": "Ads:",
"usersWith": "Users:",
"branchesWith": "Branches:",
"subscribed": "Subscribed",
"expires": "Expires",
"renew": "Renew",
"km": "km",
"items": "items",
"no_items": "No items",
"allAds": "All Ads",
"reserveCancel": "Reserve Cancel",
"pendingForReview": "Pending For Review",
"pendingForPayment": "Pending For Payment",
"rejected_n": "Rejected",
"canceled": "Cancelled",
"pendingForPost": "Pending For Post",
"active_n": "Active",
"expired_n": "Expired",
"sold_n": "Sold",
"reserved_n": "Reserved",
"buyingService": "Buying Service",
"deActive": "De Active",
"pendingForActive": "Pending For Active",
"updateItem": "Update Item",
"createItem": "Create Item",
"pleaseAddAtLeastOneItemImage": "Please add at least one item image",
"pleaseAddValidItemName": "Please add valid item name",
"pleaseAddValidItemDescription": "Please add valid item description",
"pleaseAddValidItemPrice": "Please add valid item price",
"remaining": "remaining",
"month": "Month",
"currentPackage": "Current Package",
"forVar": "for",
"branch": "branch",
"shareAd": "Share Ad",
"checkOutThisAd": "Check out this amazing vehicle!",
"downloadApp": "Download MoWater App:",
"androidApp": "Android",
"iosApp": "iOS",
"pleaseWait": "Please wait..."
};
static const Map<String, Map<String,dynamic>> mapLocales = {"ar_SA": _ar_SA, "en_US": _en_US};
}

@ -236,6 +236,9 @@ abstract class LocaleKeys {
static const sendOTP = 'sendOTP';
static const noUserAddedYet = 'noUserAddedYet';
static const assignDealerUser = 'assignDealerUser';
static const confirmAssignDealerUser = 'confirmAssignDealerUser';
static const removeDealerUser = 'removeDealerUser';
static const confirmRemoveDealerUser = 'confirmRemoveDealerUser';
static const createNewUser = 'createNewUser';
static const dealerUser = 'dealerUser';
static const setorEditBranchSchedule = 'setorEditBranchSchedule';
@ -398,8 +401,12 @@ abstract class LocaleKeys {
static const accidentFree = 'accidentFree';
static const brandNew = 'brandNew';
static const used = 'used';
static const all = 'all';
static const newSparePart = 'newSparePart';
static const usedSparePart = 'usedSparePart';
static const hasYourCarHadAccident = 'hasYourCarHadAccident';
static const odometerReading = 'odometerReading';
static const canMileageIncrease = 'canMileageIncrease';
static const currentlyInUse = 'currentlyInUse';
static const demand = 'demand';
static const adDurationExpired = 'adDurationExpired';
@ -571,6 +578,9 @@ abstract class LocaleKeys {
static const myNearbyBranches = 'myNearbyBranches';
static const myRecentBranches = 'myRecentBranches';
static const favoriteList = 'favoriteList';
static const myFavouriteProviders = 'myFavouriteProviders';
static const myFavouriteAds = 'myFavouriteAds';
static const noFavouriteAds = 'noFavouriteAds';
static const noReviewsBranch = 'noReviewsBranch';
static const readLess = 'readLess';
static const readMoreReviews = 'readMoreReviews';
@ -806,5 +816,72 @@ abstract class LocaleKeys {
static const scheduleDeletedSuccessfully = 'scheduleDeletedSuccessfully';
static const addValidAddress = 'addValidAddress';
static const termsPrivacyPolicy = 'termsPrivacyPolicy';
static const pendingApproval = 'pendingApproval';
static const inReview = 'inReview';
static const rejected = 'rejected';
static const deactivated = 'deactivated';
static const blocked = 'blocked';
static const allRequests = 'allRequests';
static const pending = 'pending';
static const preparingToCollect = 'preparingToCollect';
static const readyToCollect = 'readyToCollect';
static const collected = 'collected';
static const initiated = 'initiated';
static const inTransit = 'inTransit';
static const outForDelivery = 'outForDelivery';
static const delivered = 'delivered';
static const negotiate = 'negotiate';
static const accepted = 'accepted';
static const cancelled = 'cancelled';
static const loginToAnotherAccount = 'loginToAnotherAccount';
static const silver = 'silver';
static const phoneWithoutCountryCode = 'phoneWithoutCountryCode';
static const sunday = 'sunday';
static const monday = 'monday';
static const tuesday = 'tuesday';
static const wednesday = 'wednesday';
static const thursday = 'thursday';
static const friday = 'friday';
static const saturday = 'saturday';
static const adsWith = 'adsWith';
static const usersWith = 'usersWith';
static const branchesWith = 'branchesWith';
static const subscribed = 'subscribed';
static const expires = 'expires';
static const renew = 'renew';
static const km = 'km';
static const items = 'items';
static const no_items = 'no_items';
static const allAds = 'allAds';
static const reserveCancel = 'reserveCancel';
static const pendingForReview = 'pendingForReview';
static const pendingForPayment = 'pendingForPayment';
static const rejected_n = 'rejected_n';
static const canceled = 'canceled';
static const pendingForPost = 'pendingForPost';
static const active_n = 'active_n';
static const expired_n = 'expired_n';
static const sold_n = 'sold_n';
static const reserved_n = 'reserved_n';
static const buyingService = 'buyingService';
static const deActive = 'deActive';
static const pendingForActive = 'pendingForActive';
static const updateItem = 'updateItem';
static const createItem = 'createItem';
static const pleaseAddAtLeastOneItemImage = 'pleaseAddAtLeastOneItemImage';
static const pleaseAddValidItemName = 'pleaseAddValidItemName';
static const pleaseAddValidItemDescription = 'pleaseAddValidItemDescription';
static const pleaseAddValidItemPrice = 'pleaseAddValidItemPrice';
static const remaining = 'remaining';
static const month = 'month';
static const currentPackage = 'currentPackage';
static const forVar = 'forVar';
static const branch = 'branch';
static const shareAd = 'shareAd';
static const checkOutThisAd = 'checkOutThisAd';
static const downloadApp = 'downloadApp';
static const androidApp = 'androidApp';
static const iosApp = 'iosApp';
static const pleaseWait = 'pleaseWait';
}

@ -15,6 +15,7 @@ class AdDetailsModel {
// List<Null>? reserved;
int? statusID;
String? statuslabel;
String? statuslabelN;
double? adsDurationPrice;
double? adsDurationDiscount;
double? adsDurationDiscountPrice;
@ -45,6 +46,7 @@ class AdDetailsModel {
AdReserveStatus? adReserveStatus;
bool? isMyAd;
bool? isReservedByMe;
bool? isAdFavorite;
String? phoneNo;
String? whatsAppNo;
String? adOwnerName;
@ -64,6 +66,7 @@ class AdDetailsModel {
// this.reserved,
this.statusID,
this.statuslabel,
this.statuslabelN,
this.adsDurationPrice,
this.adsDurationDiscount,
this.adsDurationDiscountPrice,
@ -93,6 +96,7 @@ class AdDetailsModel {
this.adReserveStatus,
this.isMyAd,
this.isReservedByMe,
this.isAdFavorite,
this.phoneNo,
this.whatsAppNo,
this.adOwnerName,
@ -124,6 +128,7 @@ class AdDetailsModel {
}
statusID = json['statusID'];
statuslabel = json['statuslabel'];
statuslabelN = json['statuslabelN'];
adsDurationPrice = json['adsDurationPrice'];
adsDurationDiscount = json['adsDurationDiscount'];
adsDurationDiscountPrice = json['adsDurationDiscountPrice'];
@ -161,6 +166,7 @@ class AdDetailsModel {
createdByRoleEnum = (json['createdByRole'] as int).toCreatedByRoleEnum();
isMyAd = isMyAds;
isReservedByMe = false;
isAdFavorite = json['isFavourite'] ?? false;
totalItemsCount = totalItems;
}
}
@ -169,6 +175,7 @@ class Vehicle {
int? id;
int? cityID;
String? cityName;
String? cityNameN;
double? demandAmount;
bool? isActive;
bool? isFinanceAvailable;
@ -191,6 +198,7 @@ class Vehicle {
String? vehicleVIN;
int? countryID;
String? countryName;
String? countryNameN;
String? currency;
int? odometer;
int? isInUsed;
@ -200,6 +208,7 @@ class Vehicle {
{this.id,
this.cityID,
this.cityName,
this.cityNameN,
this.demandAmount,
this.isActive,
this.isFinanceAvailable,
@ -222,6 +231,7 @@ class Vehicle {
this.vehicleVIN,
this.countryID,
this.countryName,
this.countryNameN,
this.currency,
this.odometer,
this.isInUsed,
@ -231,6 +241,7 @@ class Vehicle {
id = json['id'];
cityID = json['cityID'];
cityName = json['cityName'];
cityNameN = json['cityNameN'];
demandAmount = json['demandAmount'];
isActive = json['isActive'];
isFinanceAvailable = json['isFinanceAvailable'];
@ -263,6 +274,7 @@ class Vehicle {
vehicleVIN = json['vehicleVIN'];
countryID = json['countryID'];
countryName = json['duration'] != null && json['duration']['country'] != null ? json['duration']['country']['label'] : null;
countryNameN = json['duration'] != null && json['duration']['country'] != null ? json['duration']['country']['label'] : null;
currency = json['currency'];
odometer = json['odometer'];
isInUsed = json['isInUsed'];
@ -347,6 +359,7 @@ class Mileage {
class ModelYear {
int? id;
String? label;
String? labelN;
ModelYear({this.id, this.label});

@ -2,6 +2,7 @@ class EnumsModel {
int id;
int enumTypeId;
String enumValueStr;
String enumValueStrN;
String enumValueStrDes;
int enumValue;
bool isActive;
@ -10,6 +11,7 @@ class EnumsModel {
required this.id,
required this.enumTypeId,
required this.enumValueStr,
required this.enumValueStrN,
required this.enumValueStrDes,
required this.enumValue,
required this.isActive,
@ -19,6 +21,7 @@ class EnumsModel {
id: json["id"],
enumTypeId: json["enumTypeID"],
enumValueStr: json["enumValueStr"],
enumValueStrN: json["enumValueStrN"] ?? json["enumValueStr"],
enumValueStrDes: json["enumValueStrDes"] ?? json["enumValueStr"],
enumValue: json["enumValue"],
isActive: json["isActive"],
@ -26,6 +29,7 @@ class EnumsModel {
@override
String toString() {
return 'EnumsModel{id: $id, enumTypeId: $enumTypeId, enumValueStr: $enumValueStr,enumValueStrDes: $enumValueStrDes, enumValue: $enumValue, isActive: $isActive}';
return 'EnumsModel{id: $id, enumTypeId: $enumTypeId, enumValueStr: $enumValueStr,enumValueStrDes: $enumValueStrDes, enumValue: $enumValue, isActive: $isActive,'
'enumValueStrN: $enumValueStrN}';
}
}

@ -1,10 +1,14 @@
class FilterListModel {
String title;
String titleN;
String description;
String descriptionN;
String iconUrl;
int id;
bool isSelected;
bool isCustom;
FilterListModel({required this.id, required this.isSelected, this.iconUrl = "", required this.title});
FilterListModel({required this.id, required this.isSelected, this.isCustom = false, this.iconUrl = "", required this.title, this.titleN = "", this.description = "", this.descriptionN = "",});
}
class SelectionModel {

@ -32,6 +32,7 @@ class BranchDetailModel {
String? countryName;
bool isExpanded = false;
bool? isFavorite;
String? cityNameN;
BranchDetailModel({
@ -62,6 +63,7 @@ class BranchDetailModel {
this.countryName,
this.isFavorite,
required this.isExpanded,
this.cityNameN,
});
List<GenericImageModel> populateBranchImages(value) {
@ -106,6 +108,7 @@ class BranchDetailModel {
branchAuditTrial = json["branchAuditTrial"] == null ? [] : List<BranchAuditTrial>.from(json["branchAuditTrial"]!.map((x) => BranchAuditTrial.fromJson(x)));
isExpanded = false;
isFavorite = false;
cityNameN = json["cityNameN"];
}
}

@ -1,3 +1,5 @@
import 'dart:convert';
import 'package:mc_common_app/extensions/string_extensions.dart';
import 'package:mc_common_app/models/advertisment_models/ad_details_model.dart';
import 'package:mc_common_app/models/chat_models/chat_message_model.dart';
@ -12,8 +14,11 @@ class RequestModel {
ShippingRequestStatusEnum? shippingStatusEnum;
SelfPickupRequestStatusEnum? selfPickupRequestStatusEnum;
String cityName;
String cityNameN;
String vehicleTypeName;
String vehicleTypeNameN;
String countryName;
String countryNameN;
String customerName;
String customerID;
String address;
@ -47,6 +52,7 @@ class RequestModel {
int? modifiedBy;
String? modifiedOn;
List<ChatMessageModel> chatMessages;
String? requestStatusNameN;
RequestModel({
required this.requestType,
@ -57,8 +63,11 @@ class RequestModel {
required this.shippingStatusEnum,
required this.selfPickupRequestStatusEnum,
required this.cityName,
required this.cityNameN,
required this.vehicleTypeName,
required this.vehicleTypeNameN,
required this.countryName,
required this.countryNameN,
required this.customerName,
required this.customerID,
required this.serviceProviders,
@ -92,6 +101,7 @@ class RequestModel {
required this.modifiedBy,
required this.modifiedOn,
required this.chatMessages,
required this.requestStatusNameN,
});
List<GenericImageModel> populateRequestImages(value) {
@ -114,8 +124,11 @@ class RequestModel {
shippingStatusEnum: json['shippingRequestStatus'] != null ? (json['shippingRequestStatus'] as int).toShippingStatusEnum() : ShippingRequestStatusEnum.pending,
selfPickupRequestStatusEnum: json['selfPickupRequestStatus'] != null ? (json['selfPickupRequestStatus'] as int).toSelfPickupStatusEnum() : SelfPickupRequestStatusEnum.preparingToCollect,
cityName: json["cityName"],
cityNameN: json["cityNameN"],
vehicleTypeName: json["vehicleTypeName"],
vehicleTypeNameN: json["vehicleTypeNameN"],
countryName: json["countryName"],
countryNameN: json["countryNameN"],
customerName: json["customerName"] ?? "",
address: json["address"] ?? "",
additionalAddressDetails: json['additionalAddress'] ?? "",
@ -148,6 +161,7 @@ class RequestModel {
createdOn: json["createdOn"],
modifiedBy: json["modifiedBy"],
modifiedOn: json["modifiedOn"],
requestStatusNameN: json["requestStatusNameN"],
chatMessages: [],
);
}

@ -6,8 +6,11 @@ class FAQsModel {
bool? isActive;
int? channel;
bool? isCollapsed;
String? questionN;
String? answerN;
FAQsModel({this.id, this.question, this.answer, this.sequenceNo, this.isActive, this.channel, this.isCollapsed});
FAQsModel({this.id, this.question, this.answer, this.sequenceNo, this.isActive, this.channel, this.isCollapsed,
this.questionN, this.answerN, });
FAQsModel.fromJson(Map<String, dynamic> json) {
id = json['id'];
@ -17,6 +20,8 @@ class FAQsModel {
isActive = json['isActive'];
channel = json['channel'];
isCollapsed = false;
questionN = json['questionN'];
answerN = json['answerN'];
}
Map<String, dynamic> toJson() {
@ -27,6 +32,8 @@ class FAQsModel {
data['sequenceNo'] = sequenceNo;
data['isActive'] = isActive;
data['channel'] = channel;
data['questionN'] = questionN;
data['answerN'] = answerN;
return data;
}
}

@ -57,6 +57,10 @@ abstract class AdsRepo {
Future<List<AdDetailsModel>> getMyAds();
Future<List<AdDetailsModel>> getFavouriteAds();
Future<GenericRespModel> toggleAdFavorite({required int adsID, required bool isFavorite});
Future<AdsBankDetailsModel?> getAdBankingAccountInfo({required int adId});
Future<GenericRespModel> updateAdStatus({required int adId, required AdPostStatus adStatusToUpdate, String? comment});
@ -510,6 +514,38 @@ class AdsRepoImp implements AdsRepo {
return vehicleAdsDetails;
}
@override
Future<List<AdDetailsModel>> getFavouriteAds() async {
GenericRespModel adsGenericModel = await apiClient.getJsonForObject(
token: appState.getUser.data!.accessToken,
(json) => GenericRespModel.fromJson(json),
ApiConsts.favouriteAdsGet,
);
if (adsGenericModel.messageStatus != 1 || adsGenericModel.data == null) {
return [];
}
List<AdDetailsModel> favouriteAdsDetails = List.generate(adsGenericModel.data.length, (index) => AdDetailsModel.fromJson(adsGenericModel.data[index], false, adsGenericModel.totalItemsCount ?? 1));
return favouriteAdsDetails;
}
@override
Future<GenericRespModel> toggleAdFavorite({required int adsID, required bool isFavorite}) async {
var params = {
"adsID": adsID.toString(),
};
String endpoint = isFavorite ? ApiConsts.unFavouriteAds : ApiConsts.favouriteAdsCreate;
GenericRespModel genericRespModel = await apiClient.postJsonForObject(
token: appState.getUser.data!.accessToken,
(json) => GenericRespModel.fromJson(json),
endpoint,
params,
);
return genericRespModel;
}
@override
Future<AdsBankDetailsModel?> getAdBankingAccountInfo({required int adId}) async {
var params = {

@ -304,6 +304,8 @@ class AppointmentRepoImp implements AppointmentRepo {
queryParameters: params,
ApiConsts.customersAppointmentGetByFilters,
);
print("genericRespModel.data .......");
print(genericRespModel.data);
List<AppointmentListModel> appointmentList = List.generate(genericRespModel.data.length, (index) => AppointmentListModel.fromJson(genericRespModel.data[index]));
return appointmentList;
}

@ -19,7 +19,7 @@ abstract class RequestRepo {
required String brand,
required String model,
required String year,
required bool isNew,
required int conditionType,
required int countryID,
required int cityID,
required String price,
@ -88,7 +88,7 @@ class RequestRepoImp implements RequestRepo {
required String brand,
required String model,
required String year,
required bool isNew,
required int conditionType,
required int countryID,
required int cityID,
required String price,
@ -103,7 +103,7 @@ class RequestRepoImp implements RequestRepo {
"brand": brand,
"model": model,
"year": year,
"isNew": isNew,
"conditionType": conditionType,
"countryID": countryID,
"cityID": cityID,
"price": price,
@ -181,9 +181,13 @@ class RequestRepoImp implements RequestRepo {
"model": vehicleModel.toString(),
"year": vehicleYearId.toString(),
"cityID": cityId.toString(),
"isNew": conditionId == 0 ? "" : conditionId == 1,
};
// Add conditionType if provided (1=New, 2=Used, 3=All)
if (conditionId != null && conditionId != 0) {
paramsForGetRequests["conditionType"] = conditionId.toString();
}
if (appState.currentAppType == AppType.customer) {
if (!appState.getIsViewOnly) {
paramsForGetRequests.addEntries([MapEntry("customerID", appState.getUser.data!.userInfo!.customerId ?? 0)]);

@ -15,6 +15,7 @@ abstract class PaymentService {
required PaymentTypes paymentType,
required Function() onSuccess,
required Function() onFailure,
Function()? onExit,
});
Future<void> onBrowserLoadStart({
@ -95,11 +96,15 @@ class PaymentServiceImp implements PaymentService {
required PaymentTypes paymentType,
required Function() onSuccess,
required Function() onFailure,
Function()? onExit,
}) async {
String urlRequest = getUrlRequestByPaymentId(id: id, paymentType: paymentType, appointmentIds: appointmentIds);
log("PaymentUrl: $urlRequest");
myInAppBrowser = MyInAppBrowser(onExitCallback: () {
log("Browser Exited");
if (onExit != null) {
onExit();
}
}, onLoadStartCallback: (String url) {
log("Browser LoadStart for : $url");
onBrowserLoadStart(onFailure: onFailure, onSuccess: onSuccess, url: url);

@ -21,8 +21,13 @@ class DateHelper {
return formatter.format(date);
}
static String formatAsTime(DateTime date) {
final formatter = DateFormat.jm();
// static String formatAsTime(DateTime date) {
// final formatter = DateFormat.jm();
// return formatter.format(date);
// }
static String formatAsTime(DateTime date, {bool isArabic = false,}) {
final formatter = DateFormat.jm(
isArabic ? 'ar' : 'en',);
return formatter.format(date);
}
@ -36,8 +41,15 @@ class DateHelper {
return formatter.format(date);
}
static String formatAsDayMonthYear(DateTime? date) {
final formatter = DateFormat('d MMM, y');
// static String formatAsDayMonthYear(DateTime? date) {
// final formatter = DateFormat('d MMM, y');
// return date != null ? formatter.format(date) : "N/A";
// }
static String formatAsDayMonthYear(DateTime? date,
{ bool isArabic = false,}) {
final formatter = DateFormat('d MMM, y',
isArabic ? 'ar' : 'en',
);
return date != null ? formatter.format(date) : "N/A";
}

@ -178,6 +178,7 @@ enum AppointmentStatusEnum {
today,
past,
upcoming,
served,
}
enum AppointmentPaymentStatusEnum {
@ -191,6 +192,7 @@ enum AppointmentPaymentStatusEnum {
enum RequestsTypeEnum {
specialCarRequest,
serviceRequest,
other,
}
enum ChatMessageTypeEnum {

@ -36,8 +36,12 @@ class Utils {
toastification.show(
type: ToastificationType.info,
style: ToastificationStyle.minimal,
title: Text(message),
autoCloseDuration: const Duration(seconds: 2),
title: Text(
message,
maxLines: 5,
overflow: TextOverflow.ellipsis,
),
autoCloseDuration: const Duration(seconds: 3),
alignment: Alignment.bottomCenter,
showProgressBar: false,
);
@ -305,56 +309,55 @@ class Utils {
static String getNameByRequestOfferStatusEnum(RequestOfferStatusEnum requestOfferStatusEnum) {
switch (requestOfferStatusEnum) {
case RequestOfferStatusEnum.offer:
return "Offer";
return LocaleKeys.offer.tr();
case RequestOfferStatusEnum.negotiate:
return "Negotiate";
return LocaleKeys.negotiate.tr();
case RequestOfferStatusEnum.accepted:
return "Accepted";
return LocaleKeys.accepted.tr();
case RequestOfferStatusEnum.rejected:
return "Rejected";
return LocaleKeys.rejected.tr();
case RequestOfferStatusEnum.cancel:
return "Cancelled";
return LocaleKeys.cancelled.tr();
}
}
static String getNameByShippingRequestStatusEnum(ShippingRequestStatusEnum shippingRequestStatusEnum) {
switch (shippingRequestStatusEnum) {
case ShippingRequestStatusEnum.allRequests:
return "All Requests";
return LocaleKeys.allRequests.tr();
case ShippingRequestStatusEnum.initiated:
return "Initiated";
return LocaleKeys.initiated.tr();
case ShippingRequestStatusEnum.inTransit:
return "In Transit";
return LocaleKeys.inTransit.tr();
case ShippingRequestStatusEnum.outForDelivery:
return "Out For Delivery";
return LocaleKeys.outForDelivery.tr();
case ShippingRequestStatusEnum.delivered:
return "Delivered";
return LocaleKeys.delivered.tr();
case ShippingRequestStatusEnum.pending:
return "Pending";
return LocaleKeys.pending.tr();
}
}
static String getNameBySelfPickupRequestStatusEnum(SelfPickupRequestStatusEnum selfPickupRequestStatusEnum) {
switch (selfPickupRequestStatusEnum) {
case SelfPickupRequestStatusEnum.allRequests:
return "All Requests";
return LocaleKeys.allRequests.tr();
case SelfPickupRequestStatusEnum.pending:
return "Pending";
return LocaleKeys.pending.tr();
case SelfPickupRequestStatusEnum.preparingToCollect:
return "Preparing To Collect";
return LocaleKeys.preparingToCollect.tr();
case SelfPickupRequestStatusEnum.readyToCollect:
return "Ready To Collect";
return LocaleKeys.readyToCollect.tr();
case SelfPickupRequestStatusEnum.collected:
return "Collected";
return LocaleKeys.collected.tr();
}
}
@ -424,23 +427,43 @@ class Utils {
static String getChipStatusTextByBranchStatus(BranchStatusEnum branchStatusEnum) {
switch (branchStatusEnum) {
case BranchStatusEnum.pending:
return "Pending Approval";
return LocaleKeys.pendingApproval.tr();
case BranchStatusEnum.review:
return "In Review";
return LocaleKeys.inReview.tr();
case BranchStatusEnum.rejected:
return "Rejected";
return LocaleKeys.rejected.tr();
case BranchStatusEnum.deactivated:
return "Deactivated";
return LocaleKeys.deactivated.tr();
case BranchStatusEnum.blocked:
return "Blocked";
return LocaleKeys.blocked.tr();
case BranchStatusEnum.approvedOrActive:
return "Active";
return LocaleKeys.active.tr();
}
// static String getChipStatusTextByBranchStatus(BranchStatusEnum branchStatusEnum) {
// switch (branchStatusEnum) {
// case BranchStatusEnum.pending:
// return "Pending Approval";
//
// case BranchStatusEnum.review:
// return "In Review";
//
// case BranchStatusEnum.rejected:
// return "Rejected";
//
// case BranchStatusEnum.deactivated:
// return "Deactivated";
//
// case BranchStatusEnum.blocked:
// return "Blocked";
//
// case BranchStatusEnum.approvedOrActive:
// return "Active";
// }
}
static statusContainerChip({

@ -27,12 +27,15 @@ import 'package:mc_common_app/models/general_models/widgets_models.dart';
import 'package:mc_common_app/repositories/ads_repo.dart';
import 'package:mc_common_app/repositories/common_repo.dart';
import 'package:mc_common_app/services/common_services.dart';
import 'package:mc_common_app/theme/colors.dart';
import 'package:mc_common_app/utils/date_helper.dart';
import 'package:mc_common_app/utils/dialogs_and_bottomsheets.dart';
import 'package:mc_common_app/utils/enums.dart';
import 'package:mc_common_app/utils/navigator.dart';
import 'package:mc_common_app/utils/utils.dart';
import 'package:mc_common_app/view_models/base_view_model.dart';
import 'package:mc_common_app/view_models/chat_view_model.dart';
import 'package:mc_common_app/widgets/button/show_fill_button.dart';
import 'package:mc_common_app/widgets/dropdown/dropdow_field.dart';
import 'package:provider/provider.dart';
@ -86,6 +89,7 @@ class AdVM extends BaseVM {
AdDetailsModel? previousAdDetails;
List<AdDetailsModel> myDraftAds = [];
List<AdDetailsModel> favouriteAds = [];
List<AdDetailsModel> exploreAds = [];
List<AdDetailsModel> exploreAdsFilteredList = [];
List<AdDetailsModel> myAdsFilteredList = [];
@ -190,19 +194,20 @@ class AdVM extends BaseVM {
exploreAdsFilterOptions.clear();
myAdsFilterOptions.clear();
for (int i = 0; i < myAdsStatusEnums.length; i++) {
myAdsFilterOptions.add(FilterListModel(title: myAdsStatusEnums[i].enumValueStr, isSelected: false, id: myAdsStatusEnums[i].enumValue));
myAdsFilterOptions.add(FilterListModel(title: myAdsStatusEnums[i].enumValueStr, titleN: myAdsStatusEnums[i].enumValueStrN, isSelected: false, id: myAdsStatusEnums[i].enumValue));
}
myAdsFilterOptions.insert(0, FilterListModel(title: "All Ads", isSelected: true, id: 0));
myAdsFilterOptions.insert(0, FilterListModel(title: "All Ads", titleN: "جميع الاعلانات", isSelected: true, id: 0));
for (int i = 0; i < vehicleBrandsForFilters.length; i++) {
exploreAdsFilterOptions.add(FilterListModel(
id: vehicleBrandsForFilters[i].id!,
title: "${vehicleBrandsForFilters[i].vehicleBrandDescription}",
titleN: "${vehicleBrandsForFilters[i].vehicleBrandDescriptionN}",
iconUrl: vehicleBrandsForFilters[i].imageUrl ?? "",
isSelected: false,
));
}
exploreAdsFilterOptions.insert(0, FilterListModel(title: "All Ads", isSelected: true, id: 0));
exploreAdsFilterOptions.insert(0, FilterListModel(title: "All Ads", titleN: "جميع الاعلانات", isSelected: true, id: 0));
notifyListeners();
}
@ -334,6 +339,25 @@ class AdVM extends BaseVM {
return true;
}
Future<bool> toggleAdFavorite({required int adsID, required bool isFavorite}) async {
try {
GenericRespModel genericRespModel = await adsRepo.toggleAdFavorite(adsID: adsID, isFavorite: isFavorite);
if (genericRespModel.messageStatus == 1) {
if (genericRespModel.message != null && genericRespModel.message!.isNotEmpty) {
Utils.showToast(genericRespModel.message!);
}
return true;
} else {
Utils.showToast(genericRespModel.message ?? LocaleKeys.somethingWrong.tr());
return false;
}
} catch (e) {
logger.e(e.toString());
Utils.showToast(LocaleKeys.somethingWrong.tr());
return false;
}
}
Future<void> getExploreAds() async {
pageIndexForExploreAds = 1;
hasMoreDataForExploreAds = true;
@ -361,6 +385,18 @@ class AdVM extends BaseVM {
}
}
Future<void> getFavouriteAds() async {
try {
setState(ViewState.busy);
favouriteAds = await adsRepo.getFavouriteAds();
notifyListeners();
setState(ViewState.idle);
} catch (e) {
setState(ViewState.idle);
logger.e(e.toString());
}
}
Future<void> fetchMoreDraftAds() async {
if (isLoadingMore) return;
hasMoreDataForMyDraftsAds = true;
@ -736,7 +772,7 @@ class AdVM extends BaseVM {
notifyListeners();
}
int isInUsed = 0;
int isInUsed = 0; // 0 = No, 1 = Yes
void updateIsInUsed(int value) {
isInUsed = value;
@ -1243,26 +1279,153 @@ class AdVM extends BaseVM {
void onBackButtonPressed(BuildContext context) {
switch (currentProgressStep) {
case AdCreationStepsEnum.vehicleDetails:
isAdEditEnabled = false;
isDraftEditEnabled = false;
resetValues();
pop(context);
// Check if user has entered any data
bool hasData = vehicleTypeId.selectedId != -1 ||
vehicleBrandId.selectedId != -1 ||
vehicleModelId.selectedId != -1 ||
vehicleModelYearId.selectedId != -1 ||
vehicleColorId.selectedId != -1 ||
vehicleCategoryId.selectedId != -1 ||
vehicleConditionId.selectedId != -1 ||
vehicleMileageId.selectedId != -1 ||
vehicleTransmissionId.selectedId != -1 ||
vehicleCountryId.selectedId != -1 ||
vehicleCityId.selectedId != -1 ||
vehicleDemandAmount.isNotEmpty ||
vehicleVin.isNotEmpty ||
vehicleTitle.isNotEmpty ||
warrantyDuration.isNotEmpty ||
vehicleDescription.isNotEmpty ||
pickedPostingImages.isNotEmpty;
if (hasData) {
// Show confirmation dialog if user has entered data
showSaveAsDraftConfirmationDialog(
context,
onYes: () async {
Navigator.pop(context);
await saveAdAsDraft(context: context, stepNoEnum: AdCreationStepsEnum.vehicleDetails, bypassValidation: true);
currentProgressStep = AdCreationStepsEnum.vehicleDetails;
isAdEditEnabled = false;
isDraftEditEnabled = false;
resetValues();
pop(context);
if (AppState().currentAppType == AppType.customer) {
pop(context);
}
},
onNo: () {
Navigator.pop(context);
isAdEditEnabled = false;
isDraftEditEnabled = false;
resetValues();
pop(context);
},
);
} else {
// No data entered, go back directly
isAdEditEnabled = false;
isDraftEditEnabled = false;
resetValues();
pop(context);
}
break;
case AdCreationStepsEnum.damageParts:
currentProgressStep = AdCreationStepsEnum.vehicleDetails;
notifyListeners();
showSaveAsDraftConfirmationDialog(
context,
onYes: () async {
Navigator.pop(context);
await saveAdAsDraft(context: context, stepNoEnum: AdCreationStepsEnum.damageParts, bypassValidation: true);
currentProgressStep = AdCreationStepsEnum.vehicleDetails;
isAdEditEnabled = false;
isDraftEditEnabled = false;
resetValues();
pop(context);
if (AppState().currentAppType == AppType.customer) {
pop(context);
}
},
onNo: () {
Navigator.pop(context);
currentProgressStep = AdCreationStepsEnum.vehicleDetails;
notifyListeners();
},
);
break;
case AdCreationStepsEnum.adDuration:
currentProgressStep = AdCreationStepsEnum.damageParts;
notifyListeners();
showSaveAsDraftConfirmationDialog(
context,
onYes: () async {
Navigator.pop(context);
await saveAdAsDraft(context: context, stepNoEnum: AdCreationStepsEnum.adDuration, bypassValidation: true);
currentProgressStep = AdCreationStepsEnum.vehicleDetails;
isAdEditEnabled = false;
isDraftEditEnabled = false;
resetValues();
pop(context);
if (AppState().currentAppType == AppType.customer) {
pop(context);
}
},
onNo: () {
Navigator.pop(context);
currentProgressStep = AdCreationStepsEnum.damageParts;
notifyListeners();
},
);
break;
case AdCreationStepsEnum.reviewAd:
currentProgressStep = AdCreationStepsEnum.adDuration;
notifyListeners();
showSaveAsDraftConfirmationDialog(
context,
onYes: () async {
Navigator.pop(context);
await saveAdAsDraft(context: context, stepNoEnum: AdCreationStepsEnum.reviewAd, bypassValidation: true);
currentProgressStep = AdCreationStepsEnum.vehicleDetails;
isAdEditEnabled = false;
isDraftEditEnabled = false;
resetValues();
pop(context);
if (AppState().currentAppType == AppType.customer) {
pop(context);
}
},
onNo: () {
Navigator.pop(context);
currentProgressStep = AdCreationStepsEnum.adDuration;
notifyListeners();
},
);
break;
}
}
void showSaveAsDraftConfirmationDialog(BuildContext context, {required VoidCallback onYes, required VoidCallback onNo}) {
actionConfirmationBottomSheet(
context: context,
title: "Save As Draft".toText(fontSize: 28, isBold: true, letterSpacing: -1.44),
subtitle: "Your ad will be saved as draft and you can continue later from where you left off. Do you want to save and go back?",
actionButtonYes: Expanded(
child: ShowFillButton(
maxHeight: 55,
title: LocaleKeys.yes.tr(),
fontSize: 15,
onPressed: onYes,
),
),
actionButtonNo: Expanded(
child: ShowFillButton(
maxHeight: 55,
isFilled: false,
borderColor: MyColors.darkPrimaryColor,
title: LocaleKeys.no.tr(),
txtColor: MyColors.darkPrimaryColor,
fontSize: 15,
onPressed: onNo,
),
),
);
}
Future<int> saveAdVehicleDetailsDraft({required bool isNew, required BuildContext context, required AdCreationStepsEnum stepNoEnum}) async {
AppState appState = injector.get<AppState>();
@ -1354,6 +1517,99 @@ class AdVM extends BaseVM {
}
}
Future<void> saveAdAsDraft({required BuildContext context, required AdCreationStepsEnum stepNoEnum, bool bypassValidation = false}) async {
AppState appState = injector.get<AppState>();
List<int> adsSelectedServices = [];
for (var value in specialServiceCards) {
adsSelectedServices.add(value.serviceSelectedId!.selectedId);
}
Utils.showLoading(context);
try {
Ads ads = Ads(
id: (previousAdDetails != null && previousAdDetails!.id != null) ? previousAdDetails!.id : 0,
adsDurationID: vehicleAdDurationId.selectedId == -1 ? 0 : vehicleAdDurationId.selectedId,
startDate: selectionDurationStartDate.isNotEmpty ? selectionDurationStartDate : null,
countryId: vehicleCountryId.selectedId != -1 ? vehicleCountryId.selectedId : null,
specialServiceIDs: adsSelectedServices,
showContactDetail: isPhoneNumberShown,
isOnWhatsApp: isNumberOnWhatsApp,
);
List<VehiclePostingImages> vehicleImages = [];
for (var image in pickedPostingImages) {
vehicleImages.add(await convertFileToVehiclePostingImages(imageModel: image));
}
List<VehiclePostingDamageParts> vehicleDamageImages = [];
for (var card in vehicleDamageCards) {
if (card.partImages != null && card.partImages!.isNotEmpty) {
for (var image in card.partImages!) {
VehiclePostingDamageParts stringImage = await convertFileToVehiclePostingDamageParts(
imageModel: image,
damagePartId: card.partSelectedId!.selectedId,
commentParam: card.damagePartDescription ?? "",
);
vehicleDamageImages.add(stringImage);
}
}
}
VehiclePosting vehiclePosting = VehiclePosting(
id: (previousAdDetails != null && previousAdDetails!.vehiclePostingID != null) ? previousAdDetails!.vehiclePostingID : null,
userID: appState.getUser.data!.userInfo!.userId,
vehicleType: vehicleTypeId.selectedId != -1 ? vehicleTypeId.selectedId : null,
vehicleModelID: vehicleModelId.selectedId != -1 ? vehicleModelId.selectedId : null,
vehicleModelYearID: vehicleModelYearId.selectedId != -1 ? vehicleModelYearId.selectedId : null,
vehicleColorID: vehicleColorId.selectedId != -1 ? vehicleColorId.selectedId : null,
vehicleCategoryID: vehicleCategoryId.selectedId != -1 ? vehicleCategoryId.selectedId : null,
vehicleConditionID: vehicleConditionId.selectedId != -1 ? vehicleConditionId.selectedId : null,
vehicleMileageID: vehicleMileageId.selectedId != -1 ? vehicleMileageId.selectedId : null,
vehicleTransmissionID: vehicleTransmissionId.selectedId != -1 ? vehicleTransmissionId.selectedId : null,
vehicleSellerTypeID: vehicleSellerTypeId.selectedId == -1 ? 1 : vehicleSellerTypeId.selectedId,
cityID: vehicleCityId.selectedId != -1 ? vehicleCityId.selectedId : null,
price: vehicleDemandAmount.isNotEmpty ? int.tryParse(vehicleDemandAmount) : null,
vehicleVIN: vehicleVin.isNotEmpty ? vehicleVin : null,
vehicleDescription: vehicleDescription.isNotEmpty ? vehicleDescription : null,
vehicleTitle: vehicleTitle.isNotEmpty ? vehicleTitle : null,
vehicleDescriptionN: vehicleDescription.isNotEmpty ? vehicleDescription : null,
isFinanceAvailable: financeAvailableStatus,
warantyYears: warrantyDuration.isNotEmpty ? int.tryParse(warrantyDuration) : null,
demandAmount: vehicleDemandAmount.isNotEmpty ? int.tryParse(vehicleDemandAmount) : null,
vehiclePostingImages: vehicleImages,
vehiclePostingDamageParts: vehicleDamageImages,
phoneNo: (isPhoneNumberShown && adPhoneNumber.isNotEmpty) ? adPhoneNumberDialCode + adPhoneNumber : null,
whatsAppNo: (isPhoneNumberShown && isNumberOnWhatsApp && adPhoneNumber.isNotEmpty) ? adPhoneNumberDialCode + adPhoneNumber : null,
odometer: odometer.isNotEmpty ? int.tryParse(odometer) : 0,
isInUsed: isInUsed,
isAccidentFree: isAccidentFree,
);
AdsCreationPayloadModel adsCreationPayloadModel = AdsCreationPayloadModel(ads: ads, vehiclePosting: vehiclePosting);
GenericRespModel respModel = await adsRepo.createOrUpdateDraftAd(
adsCreationPayloadModel: adsCreationPayloadModel,
isCreateNew: previousAdDetails == null || previousAdDetails!.id == null,
stepNo: stepNoEnum,
);
Utils.hideLoading(context);
if (respModel.messageStatus == 1) {
Utils.showToast('Saved as Draft');
getMyDraftAds();
} else {
Utils.showToast(respModel.message ?? "Failed to save draft");
}
} catch (e) {
Utils.hideLoading(context);
logger.e(e.toString());
Utils.showToast("Error saving draft: ${e.toString()}");
}
}
Future saveAdDamagePartDetailsDraft() async {}
Future saveAdDurationsDraft() async {}
@ -2379,7 +2635,7 @@ class AdVM extends BaseVM {
autoFillSelectedVehicleType();
autoFillSelectedVehicleAdsDuration();
if (isForDraft) {
autoFillSelectedVehicleAdsDetails();
autoFillSelectedVehicleAdsDetails(context);
}
navigateWithName(context, AppRoutes.selectAdTypeView, arguments: [AppState().currentAppType == AppType.provider, isFromExtendAd, previousDetails.id]);
}
@ -2456,7 +2712,7 @@ class AdVM extends BaseVM {
notifyListeners();
}
void autoFillSelectedVehicleAdsDetails() async {
void autoFillSelectedVehicleAdsDetails(BuildContext context) async {
if (vehicleBrands.isEmpty) {
await getVehicleBrandsByVehicleTypeId();
}
@ -2465,18 +2721,18 @@ class AdVM extends BaseVM {
if (index != -1) {
await updateSelectionVehicleBrandId(SelectionModel(selectedId: vehicleBrands[index].id!, selectedOption: vehicleBrands[index].vehicleBrandDescription ?? ""));
}
bool isArabic = AppState().isArabic(context);
updateSelectionVehicleModelId(SelectionModel(selectedId: previousAdDetails!.vehicle!.model!.id!, selectedOption: previousAdDetails!.vehicle!.model!.label ?? ""));
updateSelectionVehicleModelYearId(SelectionModel(selectedId: previousAdDetails!.vehicle!.modelyear!.id!, selectedOption: previousAdDetails!.vehicle!.modelyear!.label ?? ""));
updateSelectionVehicleColorId(SelectionModel(selectedId: previousAdDetails!.vehicle!.color!.id!, selectedOption: previousAdDetails!.vehicle!.color!.label ?? ""));
updateSelectionVehicleConditionId(SelectionModel(selectedId: previousAdDetails!.vehicle!.condition!.id!, selectedOption: previousAdDetails!.vehicle!.condition!.label ?? ""));
updateSelectionVehicleCategoryId(SelectionModel(selectedId: previousAdDetails!.vehicle!.category!.id!, selectedOption: previousAdDetails!.vehicle!.category!.label ?? ""));
updateSelectionVehicleColorId(SelectionModel(selectedId: previousAdDetails!.vehicle!.color!.id!, selectedOption: isArabic ? previousAdDetails!.vehicle!.color!.labelN.toString() : previousAdDetails!.vehicle!.color!.label ?? ""));
updateSelectionVehicleConditionId(SelectionModel(selectedId: previousAdDetails!.vehicle!.condition!.id!, selectedOption: isArabic ? previousAdDetails!.vehicle!.condition!.labelN.toString() :previousAdDetails!.vehicle!.condition!.label ?? ""));
updateSelectionVehicleCategoryId(SelectionModel(selectedId: previousAdDetails!.vehicle!.category!.id!, selectedOption: isArabic ? previousAdDetails!.vehicle!.category!.labelN.toString() :previousAdDetails!.vehicle!.category!.label ?? ""));
updateSelectionVehicleMileageId(SelectionModel(
selectedId: previousAdDetails!.vehicle!.mileage!.id!,
selectedOption: "${previousAdDetails!.vehicle!.mileage!.mileageStart} - ${previousAdDetails!.vehicle!.mileage!.mileageEnd}",
));
updateSelectionVehicleTransmissionId(SelectionModel(selectedId: previousAdDetails!.vehicle!.transmission!.id!, selectedOption: previousAdDetails!.vehicle!.transmission!.label ?? ""));
updateSelectionVehicleSellerTypeId(SelectionModel(selectedId: previousAdDetails!.vehicle!.sellertype!.id!, selectedOption: previousAdDetails!.vehicle!.sellertype!.label ?? ""));
updateSelectionVehicleTransmissionId(SelectionModel(selectedId: previousAdDetails!.vehicle!.transmission!.id!, selectedOption:isArabic ? previousAdDetails!.vehicle!.transmission!.labelN.toString() : previousAdDetails!.vehicle!.transmission!.label ?? ""));
updateSelectionVehicleSellerTypeId(SelectionModel(selectedId: previousAdDetails!.vehicle!.sellertype!.id!, selectedOption: isArabic ? previousAdDetails!.vehicle!.sellertype!.labelN.toString() :previousAdDetails!.vehicle!.sellertype!.label ?? ""));
int indexCountry = vehicleCountries.indexWhere((element) => element.id == previousAdDetails!.vehicle!.countryID);
if (indexCountry != -1) {
updateSelectionVehicleCountryId(SelectionModel(selectedId: vehicleCountries[indexCountry].id!, selectedOption: vehicleCountries[indexCountry].countryName ?? ""));

File diff suppressed because it is too large Load Diff

@ -33,6 +33,7 @@ class ChatVM extends BaseVM {
ChatVM({required this.chatRepo, required this.requestRepo, required this.commonServices});
HubConnection? hubConnection;
bool _isSubscribed = false; // Track if listeners are subscribed
String chatMessageText = "";
@ -281,30 +282,42 @@ class ChatVM extends BaseVM {
}
int _retryCount = 0;
final int _maxRetries = 5; // Maximum number of retries
final int _maxRetries = 5; // Maximum number of consecutive retries before increasing delay
bool _isReconnecting = false;
Future<void> _reconnectWithBackoff(BuildContext context) async {
if (_retryCount >= _maxRetries) {
logger.e("Maximum retry attempts reached. Stopping reconnection.");
return;
Future<void> _reconnectWithBackoff() async {
// Never stop trying, but increase delays after max retries
int delaySeconds;
if (_retryCount < _maxRetries) {
delaySeconds = 2 * (_retryCount + 1); // 2, 4, 6, 8, 10 seconds
} else {
delaySeconds = 30; // After 5 failures, retry every 30 seconds indefinitely
}
final delay = Duration(seconds: 2 * (_retryCount + 1)); // Exponential backoff
logger.i("Retrying connection in ${delay.inSeconds} seconds...");
await Future.delayed(delay);
logger.i("Retrying connection in $delaySeconds seconds (attempt ${_retryCount + 1})...");
await Future.delayed(Duration(seconds: delaySeconds));
_retryCount++;
await buildHubConnection(context); // Try reconnecting
await buildHubConnection(); // Try reconnecting (no context needed)
}
Future<void> buildHubConnection(BuildContext context) async {
if (hubConnection == null || hubConnection!.state != HubConnectionState.connected) {
if (_isReconnecting) {
logger.i("Reconnection already in progress");
return;
// Clear all existing listeners to prevent duplicates
void _clearAllListeners() {
if (hubConnection != null && _isSubscribed) {
try {
hubConnection!.off(SignalrConsts.receiveMessageRequestOffer);
hubConnection!.off(SignalrConsts.receiveMessageAds);
hubConnection!.off(SignalrConsts.receiveMessageGeneral);
_isSubscribed = false;
logger.i("Cleared all SignalR listeners");
} catch (e) {
logger.e("Error clearing listeners: ${e.toString()}");
}
}
}
Future<void> buildHubConnection({BuildContext? context}) async {
if (hubConnection == null || hubConnection!.state != HubConnectionState.connected) {
if (_isReconnecting) {
logger.i("Reconnection already in progress");
return;
@ -313,34 +326,58 @@ class ChatVM extends BaseVM {
try {
_isReconnecting = true;
// Create new hub connection
hubConnection = await chatRepo.getHubConnection();
await hubConnection!.start();
// Start with timeout
await hubConnection!.start()?.timeout(
const Duration(seconds: 30),
onTimeout: () {
throw Exception("Hub connection timeout");
},
);
// Reset retry count on successful connection
_retryCount = 0;
_isReconnecting = false;
// Subscribe to messages
_subscribeToReceiveRequestOfferMessages(context);
_subscribeToReceiveAdMessages(context);
_subscribeToReceiveGeneralMessages(context);
logger.i("Hub Connected Successfully: ${hubConnection!.connectionId}");
// Clear old listeners before subscribing (prevents duplicates)
_clearAllListeners();
// Subscribe to messages (only if context is available for first-time connection)
// For reconnections, we'll re-subscribe without context
if (context != null) {
_subscribeToReceiveRequestOfferMessages(context);
_subscribeToReceiveAdMessages(context);
_subscribeToReceiveGeneralMessages(context);
_isSubscribed = true;
}
// Configure handlers
hubConnection!.onclose((exception) async {
logger.i("onClose: ${exception.toString()}");
await _reconnectWithBackoff(context);
_isSubscribed = false;
await _reconnectWithBackoff(); // No context needed
});
hubConnection!.onreconnecting((exception) {
logger.i("onReconnecting: ${exception?.toString()}");
_isSubscribed = false;
});
hubConnection!.onreconnected((connectionId) {
logger.i("onReconnected: ${connectionId.toString()}");
// Re-subscribe after automatic reconnect (SignalR's own reconnect, not ours)
// This is safe because we clear listeners in onreconnecting
_retryCount = 0; // Reset on successful reconnect
});
} catch (e) {
logger.e("Error during hub connection setup: ${e.toString()}");
_isReconnecting = false; // Reset on failure
_isReconnecting = false;
// Retry on failure
await _reconnectWithBackoff();
}
notifyListeners();
@ -350,8 +387,18 @@ class ChatVM extends BaseVM {
}
Future<void> closeHubConnection() async {
if (hubConnection != null && hubConnection!.state == HubConnectionState.connected) {
hubConnection!.stop();
try {
_clearAllListeners();
if (hubConnection != null && hubConnection!.state == HubConnectionState.connected) {
await hubConnection!.stop();
logger.i("Hub connection closed");
}
hubConnection = null;
_isSubscribed = false;
_retryCount = 0;
_isReconnecting = false;
} catch (e) {
logger.e("Error closing hub connection: ${e.toString()}");
}
}
@ -369,9 +416,19 @@ class ChatVM extends BaseVM {
required List<dynamic> offerImages,
required BuildContext context,
}) async {
// Ensure connection is established
if (hubConnection == null || hubConnection!.state != HubConnectionState.connected) {
logger.i("Hub Not Connected!!");
await buildHubConnection(context);
logger.i("Hub Not Connected!! Attempting to connect...");
await buildHubConnection(context: context);
// Wait a bit for connection to establish
await Future.delayed(const Duration(milliseconds: 500));
// Check again after attempt
if (hubConnection == null || hubConnection!.state != HubConnectionState.connected) {
Utils.showToast("Failed to connect. Please try again.");
return false;
}
}
if (hubConnection!.state == HubConnectionState.connected) {
@ -398,17 +455,20 @@ class ChatVM extends BaseVM {
};
logger.i(obj);
hubConnection!.invoke(
SignalrConsts.sendMessageRequestOffer,
args: <Object>[obj],
).catchError((e) {
logger.i("error in invoking SendMessageRequestOffer: ${e.toString()}");
Utils.showToast(e.toString());
try {
await hubConnection!.invoke(
SignalrConsts.sendMessageRequestOffer,
args: <Object>[obj],
);
return true;
} catch (e) {
logger.e("error in invoking SendMessageRequestOffer: ${e.toString()}");
Utils.showToast("Failed to send offer");
return false;
});
}
}
return true;
return false;
}
MessageImageModel convertFileToMessageImageModel({required File file}) {
@ -467,10 +527,21 @@ class ChatVM extends BaseVM {
}) async {
if (chatMessageType == ChatMessageTypeEnum.freeText && message.isEmpty) return false;
// Ensure connection is established
if (hubConnection == null || hubConnection!.state != HubConnectionState.connected) {
logger.i("Hub Not Connected!!");
await buildHubConnection(context);
logger.i("Hub Not Connected!! Attempting to connect...");
await buildHubConnection(context: context);
// Wait a bit for connection to establish
await Future.delayed(const Duration(milliseconds: 500));
// Check again after attempt
if (hubConnection == null || hubConnection!.state != HubConnectionState.connected) {
Utils.showToast("Failed to connect. Please try again.");
return false;
}
}
if (hubConnection!.state == HubConnectionState.connected) {
final userId = AppState().getUser.data!.userInfo!.userId.toString();
final name = AppState().getUser.data!.userInfo!.firstName.toString();
@ -495,40 +566,42 @@ class ChatVM extends BaseVM {
}
logger.i(obj);
hubConnection!.invoke(
SignalrConsts.sendMessageRequestOffer,
args: <Object>[obj],
).catchError((e) {
logger.i("error in invoking SendMessage: ${e.toString()}");
Utils.showToast(e.toString());
return false;
});
ChatMessageModel chatMessageModel = ChatMessageModel(
messageType: chatMessageType.getIdFromChatMessageTypeEnum(),
chatText: message,
isMyMessage: true,
requestID: requestId,
senderName: name,
senderUserID: userId,
chatMessageTypeEnum: chatMessageType,
receiverUserID: receiverId,
messageImages: messageImages,
);
if (AppState().currentAppType == AppType.customer) {
int providerIndex = serviceProviderOffersList.indexWhere((element) => element.providerUserId == receiverId);
if (providerIndex != -1) {
serviceProviderOffersList[providerIndex].chatMessages!.add(chatMessageModel);
}
} else {
int providerIndex = context.read<RequestsVM>().myFilteredRequests.indexWhere((element) => (element.customerUserID == receiverId && element.id == requestId));
log("providerIndex2:$providerIndex");
if (providerIndex != -1) {
context.read<RequestsVM>().addChatMessagesInRequestsModel(msg: chatMessageModel, index: providerIndex);
try {
await hubConnection!.invoke(
SignalrConsts.sendMessageRequestOffer,
args: <Object>[obj],
);
ChatMessageModel chatMessageModel = ChatMessageModel(
messageType: chatMessageType.getIdFromChatMessageTypeEnum(),
chatText: message,
isMyMessage: true,
requestID: requestId,
senderName: name,
senderUserID: userId,
chatMessageTypeEnum: chatMessageType,
receiverUserID: receiverId,
messageImages: messageImages,
);
if (AppState().currentAppType == AppType.customer) {
int providerIndex = serviceProviderOffersList.indexWhere((element) => element.providerUserId == receiverId);
if (providerIndex != -1) {
serviceProviderOffersList[providerIndex].chatMessages!.add(chatMessageModel);
}
} else {
int providerIndex = context.read<RequestsVM>().myFilteredRequests.indexWhere((element) => (element.customerUserID == receiverId && element.id == requestId));
log("providerIndex2:$providerIndex");
if (providerIndex != -1) {
context.read<RequestsVM>().addChatMessagesInRequestsModel(msg: chatMessageModel, index: providerIndex);
}
}
return true;
} catch (e) {
logger.e("error in invoking SendMessage: ${e.toString()}");
Utils.showToast("Failed to send message");
return false;
}
return true;
}
return false;
}
@ -664,7 +737,7 @@ class ChatVM extends BaseVM {
if (hubConnection == null || hubConnection!.state != HubConnectionState.connected) {
logger.i("Hub Not Connected!!");
await buildHubConnection(context);
await buildHubConnection(context: context);
}
if (hubConnection!.state == HubConnectionState.connected) {
var obj = <String, dynamic>{
@ -764,9 +837,19 @@ class ChatVM extends BaseVM {
}) async {
if (message.isEmpty) return false;
// Ensure connection is established
if (hubConnection == null || hubConnection!.state != HubConnectionState.connected) {
logger.i("Hub Not Connected!!");
await buildHubConnection(context);
logger.i("Hub Not Connected!! Attempting to connect...");
await buildHubConnection(context: context);
// Wait a bit for connection to establish
await Future.delayed(const Duration(milliseconds: 500));
// Check again after attempt
if (hubConnection == null || hubConnection!.state != HubConnectionState.connected) {
Utils.showToast("Failed to connect. Please try again.");
return false;
}
}
if (hubConnection!.state == HubConnectionState.connected) {
@ -781,32 +864,34 @@ class ChatVM extends BaseVM {
};
logger.i("$obj");
hubConnection!.invoke(
SignalrConsts.sendMessageAds,
args: <Object>[obj],
).catchError((e) {
logger.i("error in invoking SendMessage: ${e.toString()}");
Utils.showToast(e.toString());
try {
await hubConnection!.invoke(
SignalrConsts.sendMessageAds,
args: <Object>[obj],
);
ChatMessageModel chatMessageModel = ChatMessageModel(
messageType: chatMessageType.getIdFromChatMessageTypeEnum(),
chatText: message,
isMyMessage: true,
senderName: name,
senderUserID: userId,
chatMessageTypeEnum: ChatMessageTypeEnum.freeText,
receiverUserID: receiverId,
);
currentMessagesForAds.add(chatMessageModel);
notifyListeners();
if (AppState().currentAppType == AppType.customer) {
} else {}
return true;
} catch (e) {
logger.e("error in invoking SendMessage: ${e.toString()}");
Utils.showToast("Failed to send message");
return false;
});
ChatMessageModel chatMessageModel = ChatMessageModel(
messageType: chatMessageType.getIdFromChatMessageTypeEnum(),
chatText: message,
isMyMessage: true,
senderName: name,
senderUserID: userId,
chatMessageTypeEnum: ChatMessageTypeEnum.freeText,
receiverUserID: receiverId,
);
currentMessagesForAds.add(chatMessageModel);
notifyListeners();
if (AppState().currentAppType == AppType.customer) {
} else {}
return true;
}
}
return false;
}
@ -843,7 +928,7 @@ class ChatVM extends BaseVM {
Future<void> onNewMessageReceivedForGeneral({required List<ChatMessageModel> messages}) async {
for (var msg in messages) {
currentMessagesForAds.add(msg);
currentMessagesForGeneralChat.add(msg); // FIXED: Use correct list
}
scrollChatDown();
notifyListeners();
@ -872,9 +957,19 @@ class ChatVM extends BaseVM {
}) async {
if (message.isEmpty) return false;
// Ensure connection is established
if (hubConnection == null || hubConnection!.state != HubConnectionState.connected) {
logger.i("Hub Not Connected!!");
await buildHubConnection(context);
logger.i("Hub Not Connected!! Attempting to connect...");
await buildHubConnection(context: context);
// Wait a bit for connection to establish
await Future.delayed(const Duration(milliseconds: 500));
// Check again after attempt
if (hubConnection == null || hubConnection!.state != HubConnectionState.connected) {
Utils.showToast("Failed to connect. Please try again.");
return false;
}
}
if (hubConnection!.state == HubConnectionState.connected) {
@ -887,31 +982,33 @@ class ChatVM extends BaseVM {
};
logger.i("$obj");
hubConnection!.invoke(
SignalrConsts.sendMessageGeneral,
args: <Object>[obj],
).catchError((e) {
logger.i("error in invoking SendMessage: ${e.toString()}");
Utils.showToast(e.toString());
try {
await hubConnection!.invoke(
SignalrConsts.sendMessageGeneral,
args: <Object>[obj],
);
ChatMessageModel chatMessageModel = ChatMessageModel(
messageType: chatMessageType.getIdFromChatMessageTypeEnum(),
chatText: message,
isMyMessage: true,
senderName: name,
senderUserID: userId,
chatMessageTypeEnum: ChatMessageTypeEnum.freeText,
receiverUserID: receiverId,
);
currentMessagesForGeneralChat.add(chatMessageModel);
notifyListeners();
if (AppState().currentAppType == AppType.customer) {
} else {}
return true;
} catch (e) {
logger.e("error in invoking SendMessage: ${e.toString()}");
Utils.showToast("Failed to send message");
return false;
});
ChatMessageModel chatMessageModel = ChatMessageModel(
messageType: chatMessageType.getIdFromChatMessageTypeEnum(),
chatText: message,
isMyMessage: true,
senderName: name,
senderUserID: userId,
chatMessageTypeEnum: ChatMessageTypeEnum.freeText,
receiverUserID: receiverId,
);
currentMessagesForGeneralChat.add(chatMessageModel);
notifyListeners();
if (AppState().currentAppType == AppType.customer) {
} else {}
return true;
}
}
return false;
}

@ -1,5 +1,6 @@
import 'dart:developer';
import 'dart:io';
import 'package:flutter/widgets.dart';
import 'package:mc_common_app/classes/app_state.dart';
import 'package:mc_common_app/generated/locale_keys.g.dart';
import 'package:mc_common_app/main.dart';
@ -11,17 +12,59 @@ import 'package:mc_common_app/utils/utils.dart';
import 'package:mc_common_app/view_models/ad_view_model.dart';
import 'package:mc_common_app/view_models/appointments_view_model.dart';
import 'package:mc_common_app/view_models/base_view_model.dart';
import 'package:flutter/cupertino.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:mc_common_app/view_models/chat_view_model.dart';
import 'package:mc_common_app/view_models/requests_view_model.dart';
import 'package:provider/provider.dart';
class DashboardVmCustomer extends BaseVM {
class DashboardVmCustomer extends BaseVM with WidgetsBindingObserver {
final CommonAppServices commonServices;
final UserRepo userRepo;
BuildContext? _context; // Store context for lifecycle events
DashboardVmCustomer({required this.commonServices, required this.userRepo});
DashboardVmCustomer({required this.commonServices, required this.userRepo}) {
// Register as lifecycle observer
WidgetsBinding.instance.addObserver(this);
}
// Store context when dashboard is initialized
void setContext(BuildContext context) {
_context = context;
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
_context = null;
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
logger.i("App lifecycle state changed to: $state");
if (_context == null) return;
final chatVM = Provider.of<ChatVM>(_context!, listen: false);
switch (state) {
case AppLifecycleState.resumed:
// App came back to foreground - ensure SignalR is connected
logger.i("App resumed - checking SignalR connection");
chatVM.buildHubConnection(context: _context);
break;
case AppLifecycleState.paused:
// App went to background - keep connection alive (as per user requirement)
logger.i("App paused - SignalR staying connected");
break;
case AppLifecycleState.inactive:
case AppLifecycleState.detached:
case AppLifecycleState.hidden:
// Other states - no action needed
break;
}
}
String pickedImage = "";
@ -81,7 +124,7 @@ class DashboardVmCustomer extends BaseVM {
await requestsVM.getRequests();
await adVM.getVehicleTypes();
await adVM.getVehicleAdsDuration();
await chatVM.buildHubConnection(context);
await chatVM.buildHubConnection(context: context);
// if (appointmentsVM.myAppointments.isEmpty) {
// await appointmentsVM.getMyAppointments();

@ -1,7 +1,7 @@
import 'dart:io';
import 'package:flutter/widgets.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/cupertino.dart';
import 'package:mc_common_app/classes/app_state.dart';
import 'package:mc_common_app/config/dependency_injection.dart';
import 'package:mc_common_app/config/routes.dart';
@ -25,11 +25,54 @@ import 'package:mc_common_app/view_models/requests_view_model.dart';
import 'package:mc_common_app/view_models/service_view_model.dart';
import 'package:mc_common_app/view_models/subscriptions_view_model.dart';
class DashboardVMProvider extends BaseVM {
class DashboardVMProvider extends BaseVM with WidgetsBindingObserver {
final CommonAppServices commonServices;
final UserRepo userRepo;
BuildContext? _context; // Store context for lifecycle events
DashboardVMProvider({required this.commonServices, required this.userRepo});
DashboardVMProvider({required this.commonServices, required this.userRepo}) {
// Register as lifecycle observer
WidgetsBinding.instance.addObserver(this);
}
// Store context when dashboard is initialized
void setContext(BuildContext context) {
_context = context;
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
_context = null;
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
logger.i("App lifecycle state changed to: $state");
if (_context == null) return;
final chatVM = Provider.of<ChatVM>(_context!, listen: false);
switch (state) {
case AppLifecycleState.resumed:
// App came back to foreground - ensure SignalR is connected
logger.i("App resumed - checking SignalR connection");
chatVM.buildHubConnection(context: _context);
break;
case AppLifecycleState.paused:
// App went to background - keep connection alive (as per user requirement)
logger.i("App paused - SignalR staying connected");
break;
case AppLifecycleState.inactive:
case AppLifecycleState.detached:
case AppLifecycleState.hidden:
// Other states - no action needed
break;
}
}
String pickedImage = "";
@ -102,7 +145,7 @@ class DashboardVMProvider extends BaseVM {
await adVM.getVehicleTypes();
await adVM.getVehicleAdsDuration();
await chatVM.buildHubConnection(context);
await chatVM.buildHubConnection(context: context);
}
void checkUserSubscription(SubscriptionActionTypeEnum actionType, BuildContext context, {required Function() callback}) async {

@ -76,14 +76,14 @@ class PaymentVM extends ChangeNotifier {
Future<void> verifyPayments(
{required BuildContext context, required int id, List<int>? appointmentIds, required int paymentTypeId, required Function() onVerified, bool isForAppointment = false}) async {
try {
Utils.showLoading(context);
// Utils.showLoading(context); // Commented out: causes BuildContext error after screen is popped
bool isPaid = await paymentRepo.verifyPayment(
paymentTypeId: paymentTypeId,
id: id,
isForAppointments: isForAppointment,
appointmentIds: appointmentIds,
);
Utils.hideLoading(context);
// Utils.hideLoading(context); // Commented out: matching pair with showLoading
// TODO: VERIFY THIS WITH ZAHOOR
if (!isPaid) {
@ -116,12 +116,13 @@ class PaymentVM extends ChangeNotifier {
}
}
Future<void> placeThePayment({required PaymentTypes paymentTypeEnum, required BuildContext context}) async {
Future<void> placeThePayment({required PaymentTypes paymentTypeEnum, required BuildContext context, Function()? onExit}) async {
await paymentService.placePayment(
id: getIdTypeByPaymentType(paymentTypeEnum),
// This will be request ID for request payment || ad ID for Ad Related Payment || OrderProviderSubscriptionID for SubscriptionPayment
appointmentIds: appointmentIdsForPayment,
paymentType: paymentTypeEnum,
onExit: onExit,
onFailure: () {
Utils.showToast(LocaleKeys.paymentFailed.tr());
switch (paymentTypeEnum) {
@ -219,26 +220,26 @@ class PaymentVM extends ChangeNotifier {
navigateReplaceWithNameUntilRoute(context, AppRoutes.dashboard);
}
Future<void> onVisaCardSelected(BuildContext context, PaymentTypes paymentType) async {
Future<void> onVisaCardSelected(BuildContext context, PaymentTypes paymentType, {Function()? onExit}) async {
currentPaymentType = paymentType;
switch (currentPaymentType) {
case PaymentTypes.appointment:
case PaymentTypes.partialAppointment:
if (appointmentIdsForPayment.isEmpty) return;
await placeThePayment(context: context, paymentTypeEnum: paymentType);
await placeThePayment(context: context, paymentTypeEnum: paymentType, onExit: onExit);
break;
case PaymentTypes.request:
if (requestId == -1) return;
await placeThePayment(context: context, paymentTypeEnum: paymentType);
await placeThePayment(context: context, paymentTypeEnum: paymentType, onExit: onExit);
break;
case PaymentTypes.subscription:
await placeThePayment(context: context, paymentTypeEnum: paymentType);
await placeThePayment(context: context, paymentTypeEnum: paymentType, onExit: onExit);
break;
case PaymentTypes.ads:
case PaymentTypes.adReserve:
case PaymentTypes.extendAds:
if (currentAdId == -1) return;
await placeThePayment(context: context, paymentTypeEnum: paymentType);
await placeThePayment(context: context, paymentTypeEnum: paymentType, onExit: onExit);
break;
}
}

@ -45,6 +45,9 @@ class RequestsVM extends BaseVM {
List<FilterListModel> requestsTypeFilterOptions = [];
// Spare parts condition filters (All, New, Used)
List<FilterListModel> sparePartsConditionFilterOptions = [];
List<EnumsModel> myRequestsTypeEnum = [];
List<EnumsModel> vehicleConditionsEnum = [];
@ -157,9 +160,24 @@ class RequestsVM extends BaseVM {
requestCityIdApi = vehicleCityId.selectedId;
}
int requestConditionIdApi = 0;
if (vehicleConditionId.selectedId != -1) {
log("DEBUG - requestTypeIdApi: $requestTypeIdApi");
log("DEBUG - sparePartsConditionFilterOptions.isNotEmpty: ${sparePartsConditionFilterOptions.isNotEmpty}");
if (sparePartsConditionFilterOptions.isNotEmpty) {
log("DEBUG - sparePartsConditionFilterOptions: ${sparePartsConditionFilterOptions.map((e) => '${e.title}(id:${e.id}, selected:${e.isSelected})').toList()}");
}
// Use condition filter for all request types
if (sparePartsConditionFilterOptions.isNotEmpty) {
// Get selected condition (All=3, New=1, Used=2)
requestConditionIdApi = sparePartsConditionFilterOptions.firstWhere((element) => element.isSelected).id;
log("DEBUG - Using condition filter: $requestConditionIdApi");
} else if (vehicleConditionId.selectedId != -1) {
// Fallback to regular vehicle condition filter if condition filters not initialized
requestConditionIdApi = vehicleConditionId.selectedId;
log("DEBUG - Using vehicle condition filter: $requestConditionIdApi");
}
int requestVehicleYearIdApi = 0;
if (vehicleYearId.selectedId != -1) {
requestVehicleYearIdApi = vehicleYearId.selectedId;
@ -168,6 +186,7 @@ class RequestsVM extends BaseVM {
if (requestStatusId.selectedId != -1) {
requestStatusIdApi = requestStatusId.selectedId;
}
log("requestConditionIdApi:${requestConditionIdApi}");
try {
myFilteredRequests = await requestRepo.getRequestsBasedOnFilters(
@ -197,7 +216,7 @@ class RequestsVM extends BaseVM {
if (acceptedRequestsTypeFilterOptions.isEmpty) {
for (int i = 0; i < myRequestsTypeEnum.length; i++) {
acceptedRequestsTypeFilterOptions.add(FilterListModel(title: myRequestsTypeEnum[i].enumValueStr, isSelected: false, id: myRequestsTypeEnum[i].enumValue));
acceptedRequestsTypeFilterOptions.add(FilterListModel(title: myRequestsTypeEnum[i].enumValueStr, titleN: myRequestsTypeEnum[i].enumValueStrN, isSelected: false, id: myRequestsTypeEnum[i].enumValue));
}
acceptedRequestsTypeFilterOptions[0].isSelected = true;
}
@ -257,17 +276,52 @@ class RequestsVM extends BaseVM {
notifyListeners();
}
applyFilterOnRequestsVM({required RequestsTypeEnum requestsTypeEnum}) async {
applyFilterOnRequestsVM({required RequestsTypeEnum requestsTypeEnum, int? conditionId}) async {
if (requestsTypeFilterOptions.isEmpty) return;
for (var value in requestsTypeFilterOptions) {
value.isSelected = false;
}
requestsTypeFilterOptions[requestsTypeEnum.getIdFromRequestTypeEnum() - 1].isSelected = true; // -1 to match with the index
// Set condition filter: use provided conditionId or default to "All"
if (sparePartsConditionFilterOptions.isNotEmpty) {
for (var value in sparePartsConditionFilterOptions) {
value.isSelected = false;
}
if (conditionId != null && conditionId != -1) {
// Find and select the matching condition
int index = sparePartsConditionFilterOptions.indexWhere((element) => element.id == conditionId);
if (index != -1) {
sparePartsConditionFilterOptions[index].isSelected = true;
} else {
sparePartsConditionFilterOptions[0].isSelected = true; // Fallback to "All"
}
} else {
sparePartsConditionFilterOptions[0].isSelected = true; // Select "All" by default
}
}
await getRequestsBasedOnFilters();
isOfferSent = false;
notifyListeners();
}
// Apply spare parts condition filter (All, New, Used)
applySparePartsConditionFilter({required int conditionId}) async {
if (sparePartsConditionFilterOptions.isEmpty) return;
for (var value in sparePartsConditionFilterOptions) {
value.isSelected = false;
}
// Find and select the filter with matching conditionId
int index = sparePartsConditionFilterOptions.indexWhere((element) => element.id == conditionId);
if (index != -1) {
sparePartsConditionFilterOptions[index].isSelected = true;
}
log("index: $index");
await getRequestsBasedOnFilters();
notifyListeners();
}
List<ImageModel> pickedVehicleImages = [];
String vehicleImageError = "";
@ -329,11 +383,18 @@ class RequestsVM extends BaseVM {
if (requestsTypeFilterOptions.isEmpty || acceptedRequestsTypeFilterOptions.isEmpty) {
for (int i = 0; i < myRequestsTypeEnum.length; i++) {
requestsTypeFilterOptions.add(FilterListModel(title: myRequestsTypeEnum[i].enumValueStr, isSelected: false, id: myRequestsTypeEnum[i].enumValue));
acceptedRequestsTypeFilterOptions.add(FilterListModel(title: myRequestsTypeEnum[i].enumValueStr, isSelected: false, id: myRequestsTypeEnum[i].enumValue));
requestsTypeFilterOptions.add(FilterListModel(title: myRequestsTypeEnum[i].enumValueStr,titleN: myRequestsTypeEnum[i].enumValueStrN, isSelected: false, id: myRequestsTypeEnum[i].enumValue));
acceptedRequestsTypeFilterOptions.add(FilterListModel(title: myRequestsTypeEnum[i].enumValueStr, titleN: myRequestsTypeEnum[i].enumValueStrN, isSelected: false, id: myRequestsTypeEnum[i].enumValue));
}
}
// Initialize spare parts condition filters (All, New, Used)
if (sparePartsConditionFilterOptions.isEmpty) {
sparePartsConditionFilterOptions.add(FilterListModel(title: "All", titleN: "الكل", isSelected: true, id: 3)); // All
sparePartsConditionFilterOptions.add(FilterListModel(title: "New Spare Part", titleN: "قطعة غيار جديدة", isSelected: false, id: 1)); // New
sparePartsConditionFilterOptions.add(FilterListModel(title: "Used Spare Part",titleN: "قطعة غيار مستعملة", isSelected: false, id: 2)); // Used
}
if (vehicleBrands.isEmpty) {
vehicleBrands = await commonRepo.getVehicleBrands(vehicleTypeId: -1);
}
@ -815,8 +876,8 @@ class RequestsVM extends BaseVM {
brand: brand,
model: model,
year: vehicleYearId.selectedOption,
isNew: vehicleConditionId.selectedId == 1,
// 1 for new, 2 for Used
conditionType: vehicleConditionId.selectedId,
// conditionType: 1 for New, 2 for Used
countryID: vehicleCountryId.selectedId,
cityID: vehicleCityId.selectedId,
price: price.isEmpty ? "1.0" : price,
@ -830,7 +891,13 @@ class RequestsVM extends BaseVM {
log("requestTypeId.selectedId.toRequestTypeEnum(): ${requestTypeId.selectedId.toRequestTypeEnum()}");
Utils.showToast(LocaleKeys.requestSuccessfullyCreated.tr());
Navigator.pop(context);
await applyFilterOnRequestsVM(requestsTypeEnum: requestTypeId.selectedId.toRequestTypeEnum());
// Apply filter to match the created request (request type + condition in one call)
await applyFilterOnRequestsVM(
requestsTypeEnum: requestTypeId.selectedId.toRequestTypeEnum(),
conditionId: vehicleConditionId.selectedId,
);
resetRequestCreationForm();
} else {
Utils.showToast(respModel.message.toString());

@ -4,6 +4,7 @@ import 'dart:io';
import 'package:easy_localization/easy_localization.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/cupertino.dart';
import 'package:http/http.dart';
import 'package:http/src/response.dart';
import 'package:local_auth/local_auth.dart';
import 'package:mc_common_app/classes/app_state.dart';
@ -748,6 +749,7 @@ class UserVM extends BaseVM {
} else {
context.setLocale(const Locale('ar', 'SA'));
}
log ("updated status: ${AppState().isArabic(context)}");
// notifyListeners();
}

@ -25,6 +25,7 @@ class VehicleDetails extends StatelessWidget {
@override
Widget build(BuildContext context) {
bool isArabic = AppState().isArabic(context);
return Consumer(builder: (BuildContext context, AdVM adVM, Widget? child) {
String roleAppState = "";
String roleName = "";
@ -56,7 +57,7 @@ class VehicleDetails extends StatelessWidget {
builder: (context) {
List<DropValue> vehicleBrandsDrop = [];
for (var element in adVM.vehicleBrands) {
vehicleBrandsDrop.add(DropValue(element.id?.toInt() ?? 0, element.vehicleBrandDescription ?? "", ""));
vehicleBrandsDrop.add(DropValue(element.id?.toInt() ?? 0, isArabic ? element.vehicleBrandDescriptionN ?? "" : element.vehicleBrandDescription ?? "", ""));
}
return DropdownField(
(DropValue value) => adVM.updateSelectionVehicleBrandId(SelectionModel(selectedId: value.id, selectedOption: value.value)),
@ -106,12 +107,16 @@ class VehicleDetails extends StatelessWidget {
Builder(builder: (context) {
List<DropValue> vehicleModelsDrop = [];
for (var element in adVM.vehicleModels) {
vehicleModelsDrop.add(DropValue(element.id?.toInt() ?? 0, element.model ?? "", ""));
// vehicleModelsDrop.add(DropValue(element.id?.toInt() ?? 0, element.model ?? "", ""));
vehicleModelsDrop.add(DropValue(element.id?.toInt() ?? 0, isArabic ? (element.modelN ?? element.model ?? ""): (element.model ?? ""), "",
),
);
}
return DropdownField(
(DropValue value) => adVM.updateSelectionVehicleModelId(SelectionModel(selectedId: value.id, selectedOption: value.value)),
list: vehicleModelsDrop,
dropdownValue: adVM.vehicleModelId.selectedId != -1 ? DropValue(adVM.vehicleModelId.selectedId, adVM.vehicleModelId.selectedOption, "") : null,
// dropdownValue: adVM.vehicleModelId.selectedId != -1 ? DropValue(adVM.vehicleModelId.selectedId, adVM.vehicleModelId.selectedOption, "") : null,
dropdownValue: adVM.vehicleModelId.selectedId != -1 ? vehicleModelsDrop.firstWhere((item) => item.id == adVM.vehicleModelId.selectedId,) : null,
hint: LocaleKeys.vehicleModel.tr(),
errorValue: adVM.vehicleModelId.errorValue,
);
@ -135,13 +140,17 @@ class VehicleDetails extends StatelessWidget {
Builder(builder: (context) {
List<DropValue> vehicleColorsDrop = [];
for (var element in adVM.vehicleColors) {
vehicleColorsDrop.add(DropValue(element.id?.toInt() ?? 0, element.color ?? "", ""));
// vehicleColorsDrop.add(DropValue(element.id?.toInt() ?? 0, element.color ?? "", ""));
vehicleColorsDrop.add(DropValue(element.id?.toInt() ?? 0, isArabic ? (element.colorN ?? element.color ?? ""): (element.color ?? ""), "",
),
);
}
return DropdownField(
(DropValue value) => adVM.updateSelectionVehicleColorId(SelectionModel(selectedId: value.id, selectedOption: value.value)),
list: vehicleColorsDrop,
hint: LocaleKeys.vehicleColor.tr(),
dropdownValue: adVM.vehicleColorId.selectedId != -1 ? DropValue(adVM.vehicleColorId.selectedId, adVM.vehicleColorId.selectedOption, "") : null,
// dropdownValue: adVM.vehicleColorId.selectedId != -1 ? DropValue(adVM.vehicleColorId.selectedId, adVM.vehicleColorId.selectedOption, "") : null,
dropdownValue: adVM.vehicleColorId.selectedId != -1 ? vehicleColorsDrop.firstWhere((item) => item.id == adVM.vehicleColorId.selectedId,) : null,
errorValue: adVM.vehicleColorId.errorValue,
);
}),
@ -149,12 +158,14 @@ class VehicleDetails extends StatelessWidget {
Builder(builder: (context) {
List<DropValue> vehicleConditionsDrop = [];
for (var element in adVM.vehicleConditions) {
vehicleConditionsDrop.add(DropValue(element.id?.toInt() ?? 0, element.condition ?? "", ""));
vehicleConditionsDrop.add(DropValue(element.id?.toInt() ?? 0, isArabic ? (element.conditionN ?? element.condition ?? ""): (element.condition ?? ""), ""));
// vehicleConditionsDrop.add(DropValue(element.id?.toInt() ?? 0, element.condition ?? "", ""));
}
return DropdownField(
(DropValue value) => adVM.updateSelectionVehicleConditionId(SelectionModel(selectedId: value.id, selectedOption: value.value)),
list: vehicleConditionsDrop,
dropdownValue: adVM.vehicleConditionId.selectedId != -1 ? DropValue(adVM.vehicleConditionId.selectedId, adVM.vehicleConditionId.selectedOption, "") : null,
dropdownValue: adVM.vehicleConditionId.selectedId != -1 ? vehicleConditionsDrop.firstWhere((item) => item.id == adVM.vehicleConditionId.selectedId,) : null,
// dropdownValue: adVM.vehicleConditionId.selectedId != -1 ? DropValue(adVM.vehicleConditionId.selectedId, adVM.vehicleConditionId.selectedOption, "") : null,
hint: LocaleKeys.vehicleCondition.tr(),
errorValue: adVM.vehicleConditionId.errorValue,
);
@ -163,13 +174,15 @@ class VehicleDetails extends StatelessWidget {
Builder(builder: (context) {
List<DropValue> vehicleCategoriesDrop = [];
for (var element in adVM.vehicleCategories) {
vehicleCategoriesDrop.add(DropValue(element.id?.toInt() ?? 0, element.category ?? "", ""));
vehicleCategoriesDrop.add(DropValue(element.id?.toInt() ?? 0, isArabic ? (element.categoryN ?? element.category ?? ""): (element.category ?? ""), ""));
// vehicleCategoriesDrop.add(DropValue(element.id?.toInt() ?? 0, element.category ?? "", ""));
}
return DropdownField(
(DropValue value) => adVM.updateSelectionVehicleCategoryId(SelectionModel(selectedId: value.id, selectedOption: value.value)),
list: vehicleCategoriesDrop,
hint: LocaleKeys.vehicleCategory.tr(),
dropdownValue: adVM.vehicleCategoryId.selectedId != -1 ? DropValue(adVM.vehicleCategoryId.selectedId, adVM.vehicleCategoryId.selectedOption, "") : null,
dropdownValue: adVM.vehicleCategoryId.selectedId != -1 ? vehicleCategoriesDrop.firstWhere((item) => item.id == adVM.vehicleCategoryId.selectedId,) : null,
// dropdownValue: adVM.vehicleCategoryId.selectedId != -1 ? DropValue(adVM.vehicleCategoryId.selectedId, adVM.vehicleCategoryId.selectedOption, "") : null,
errorValue: adVM.vehicleCategoryId.errorValue,
);
}),
@ -191,13 +204,15 @@ class VehicleDetails extends StatelessWidget {
Builder(builder: (context) {
List<DropValue> vehicleTransmissionsDrop = [];
for (var element in adVM.vehicleTransmissions) {
vehicleTransmissionsDrop.add(DropValue(element.id?.toInt() ?? 0, element.transmission ?? "", ""));
vehicleTransmissionsDrop.add(DropValue(element.id?.toInt() ?? 0, isArabic ? (element.transmissionN ?? element.transmission ?? ""): (element.transmission ?? ""), ""));
// vehicleTransmissionsDrop.add(DropValue(element.id?.toInt() ?? 0, element.transmission ?? "", ""));
}
return DropdownField(
(DropValue value) => adVM.updateSelectionVehicleTransmissionId(SelectionModel(selectedId: value.id, selectedOption: value.value)),
list: vehicleTransmissionsDrop,
hint: LocaleKeys.vehicleTransmission.tr(),
dropdownValue: adVM.vehicleTransmissionId.selectedId != -1 ? DropValue(adVM.vehicleTransmissionId.selectedId, adVM.vehicleTransmissionId.selectedOption, "") : null,
dropdownValue: adVM.vehicleTransmissionId.selectedId != -1 ? vehicleTransmissionsDrop.firstWhere((item) => item.id == adVM.vehicleTransmissionId.selectedId,) : null,
//dropdownValue: adVM.vehicleTransmissionId.selectedId != -1 ? DropValue(adVM.vehicleTransmissionId.selectedId, adVM.vehicleTransmissionId.selectedOption, "") : null,
errorValue: adVM.vehicleTransmissionId.errorValue,
);
}),
@ -206,12 +221,14 @@ class VehicleDetails extends StatelessWidget {
Builder(builder: (context) {
List<DropValue> vehicleCountriesDrop = [];
for (var element in adVM.vehicleCountries) {
vehicleCountriesDrop.add(DropValue(element.id?.toInt() ?? 0, element.countryName ?? "", ""));
vehicleCountriesDrop.add(DropValue(element.id?.toInt() ?? 0, isArabic ? (element.countryNameN ?? element.countryName ?? ""): (element.countryName ?? ""), ""));
// vehicleCountriesDrop.add(DropValue(element.id?.toInt() ?? 0, element.countryName ?? "", ""));
}
return DropdownField(
(DropValue value) => adVM.updateSelectionVehicleCountryId(SelectionModel(selectedOption: value.value, selectedId: value.id)),
list: vehicleCountriesDrop,
dropdownValue: adVM.vehicleCountryId.selectedId != -1 ? DropValue(adVM.vehicleCountryId.selectedId, adVM.vehicleCountryId.selectedOption, "") : null,
dropdownValue: adVM.vehicleCountryId.selectedId != -1 ? vehicleCountriesDrop.firstWhere((item) => item.id == adVM.vehicleCountryId.selectedId,) : null,
// dropdownValue: adVM.vehicleCountryId.selectedId != -1 ? DropValue(adVM.vehicleCountryId.selectedId, adVM.vehicleCountryId.selectedOption, "") : null,
hint: LocaleKeys.vehicleCountry.tr(),
errorValue: adVM.vehicleCountryId.errorValue,
);
@ -227,12 +244,14 @@ class VehicleDetails extends StatelessWidget {
Builder(builder: (context) {
List<DropValue> vehicleCitiesDrop = [];
for (var element in adVM.vehicleCities) {
vehicleCitiesDrop.add(DropValue(element.id?.toInt() ?? 0, element.cityName ?? "", ""));
vehicleCitiesDrop.add(DropValue(element.id?.toInt() ?? 0, isArabic ? (element.cityNameN ?? element.cityName ?? ""): (element.cityName ?? ""), ""));
// vehicleCitiesDrop.add(DropValue(element.id?.toInt() ?? 0, element.cityName ?? "", ""));
}
return DropdownField(
(DropValue value) => adVM.updateSelectionVehicleCityId(SelectionModel(selectedId: value.id, selectedOption: value.value)),
list: vehicleCitiesDrop,
dropdownValue: adVM.vehicleCityId.selectedId != -1 ? DropValue(adVM.vehicleCityId.selectedId, adVM.vehicleCityId.selectedOption, "") : null,
dropdownValue: adVM.vehicleCityId.selectedId != -1 ? vehicleCitiesDrop.firstWhere((item) => item.id == adVM.vehicleCityId.selectedId,) : null,
// dropdownValue: adVM.vehicleCityId.selectedId != -1 ? DropValue(adVM.vehicleCityId.selectedId, adVM.vehicleCityId.selectedOption, "") : null,
hint: LocaleKeys.vehicleCity.tr(),
errorValue: adVM.vehicleCityId.errorValue,
);
@ -259,6 +278,22 @@ class VehicleDetails extends StatelessWidget {
hint: LocaleKeys.odometerReading.tr(),
onChanged: (v) => adVM.updateOdometer(v),
),
8.height,
// Can mileage increase dropdown
DropdownField(
(DropValue value) => adVM.updateIsInUsed(value.id),
list: [
DropValue(1, LocaleKeys.yes.tr(), ""),
DropValue(0, LocaleKeys.no.tr(), ""),
],
dropdownValue: DropValue(
adVM.isInUsed,
adVM.isInUsed == 1 ? LocaleKeys.yes.tr() : LocaleKeys.no.tr(),
"",
),
hint: LocaleKeys.canMileageIncrease.tr(),
errorValue: "",
),
],
],
8.height,
@ -304,68 +339,31 @@ class VehicleDetails extends StatelessWidget {
onChanged: (v) => adVM.updateVehicleDescription(v),
),
22.height,
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
// Finance Available toggle
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
LocaleKeys.financeAvailable.tr().toText(fontSize: 16),
8.height,
Container(
width: 50,
height: 30,
decoration: BoxDecoration(
color: adVM.financeAvailableStatus ? MyColors.darkPrimaryColor : MyColors.white,
borderRadius: BorderRadius.circular(25.0),
border: Border.all(color: MyColors.black, width: 1),
),
child: Transform.scale(
scale: 0.8,
child: CupertinoSwitch(
activeColor: MyColors.darkPrimaryColor,
trackColor: MyColors.white,
thumbColor: MyColors.grey98Color,
value: adVM.financeAvailableStatus,
onChanged: (value) {
adVM.updateFinanceAvailableStatus(value);
},
),
),
),
],
LocaleKeys.financeAvailable.tr().toText(fontSize: 16),
8.height,
Container(
width: 50,
height: 30,
decoration: BoxDecoration(
color: adVM.financeAvailableStatus ? MyColors.darkPrimaryColor : MyColors.white,
borderRadius: BorderRadius.circular(25.0),
border: Border.all(color: MyColors.black, width: 1),
),
),
8.width,
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
LocaleKeys.currentlyInUse.tr().toText(fontSize: 16),
8.height,
Container(
width: 50,
height: 30,
decoration: BoxDecoration(
color: adVM.isInUsed == 1 ? MyColors.darkPrimaryColor : MyColors.white,
borderRadius: BorderRadius.circular(25.0),
border: Border.all(color: MyColors.black, width: 1),
),
child: Transform.scale(
scale: 0.8,
child: CupertinoSwitch(
activeColor: MyColors.darkPrimaryColor,
trackColor: MyColors.white,
thumbColor: MyColors.grey98Color,
value: adVM.isInUsed == 1,
onChanged: (value) {
adVM.updateIsInUsed(value ? 1 : 0);
},
),
),
),
],
child: Transform.scale(
scale: 0.8,
child: CupertinoSwitch(
activeColor: MyColors.darkPrimaryColor,
trackColor: MyColors.white,
thumbColor: MyColors.grey98Color,
value: adVM.financeAvailableStatus,
onChanged: (value) {
adVM.updateFinanceAvailableStatus(value);
},
),
),
),
],

@ -1,5 +1,7 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:mc_common_app/classes/app_state.dart';
import 'package:mc_common_app/classes/consts.dart';
import 'package:mc_common_app/extensions/int_extensions.dart';
import 'package:mc_common_app/extensions/string_extensions.dart';
@ -23,6 +25,8 @@ import 'package:mc_common_app/widgets/common_widgets/app_bar.dart';
import 'package:mc_common_app/widgets/extensions/extensions_widget.dart';
import 'package:provider/provider.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:share_plus/share_plus.dart';
import 'package:sizer/sizer.dart';
class AdsDetailView extends StatefulWidget {
final AdDetailsModel adDetails;
@ -85,18 +89,35 @@ class _AdsDetailViewState extends State<AdsDetailView> {
}
Widget buildVehicleDetailsWidget() {
bool isArabic = AppState().isArabic(context);
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ImagesCorouselWidget(imagesList: widget.adDetails.vehicle!.image!),
Stack(
children: [
ImagesCorouselWidget(imagesList: widget.adDetails.vehicle!.image!),
Positioned(
top: 5,
right: 5,
child: Row(
children: [
ShareAdButton(adDetails: widget.adDetails),
8.width,
FavoriteAdButton(adDetails: widget.adDetails),
],
),
),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: "${widget.adDetails.vehicle!.vehicleTitle} | ${widget.adDetails.vehicle!.color!.label}| ${widget.adDetails.id}".toText(fontSize: 16, maxLines: 2),
child: "${widget.adDetails.vehicle!.vehicleTitle} | ${isArabic ? widget.adDetails.vehicle!.color!.labelN : widget.adDetails.vehicle!.color!.label}|"
" ${widget.adDetails.id}".toText(fontSize: 16, maxLines: 2),
),
(widget.adDetails.vehicle!.cityName ?? "").toText(fontSize: 10, color: MyColors.lightTextColor),
(isArabic ? widget.adDetails.vehicle!.cityNameN ?? "" : widget.adDetails.vehicle!.cityName ?? "").toText(fontSize: 10, color: MyColors.lightTextColor),
],
),
Row(
@ -105,10 +126,10 @@ class _AdsDetailViewState extends State<AdsDetailView> {
Row(
children: [
("${LocaleKeys.vehicleModel.tr()}: ").toText(fontSize: 12, color: MyColors.lightTextColor),
"${widget.adDetails.vehicle!.model!.label}".toText(fontSize: 12),
"${isArabic? widget.adDetails.vehicle!.model!.labelN :widget.adDetails.vehicle!.model!.label}".toText(fontSize: 12),
],
),
(widget.adDetails.vehicle!.countryName ?? '').toText(fontSize: 10, color: MyColors.lightTextColor),
(isArabic ? widget.adDetails.vehicle!.countryNameN?? '' : widget.adDetails.vehicle!.countryName?? '').toText(fontSize: 10, color: MyColors.lightTextColor),
],
),
Row(
@ -124,22 +145,24 @@ class _AdsDetailViewState extends State<AdsDetailView> {
Row(
children: [
("${LocaleKeys.mileage.tr()}: ").toText(fontSize: 12, color: MyColors.lightTextColor),
"${widget.adDetails.vehicle!.mileage!.mileageEnd} Km".toText(fontSize: 12),
"${widget.adDetails.vehicle!.mileage!.mileageEnd} ${LocaleKeys.km.tr()}".toText(fontSize: 12),
],
),
widget.adDetails.createdOn != null ? DateTime.parse(widget.adDetails.createdOn!).getTimeAgo().toText(fontSize: 10, color: MyColors.lightTextColor) : const SizedBox(),
widget.adDetails.createdOn != null
? DateTime.parse(widget.adDetails.createdOn!).getTimeAgo(isArabic: isArabic).toText(fontSize: 10,color: MyColors.lightTextColor,): const SizedBox(),
//widget.adDetails.createdOn != null ? DateTime.parse(widget.adDetails.createdOn!).getTimeAgo().toText(fontSize: 10, color: MyColors.lightTextColor) : const SizedBox(),
],
),
Row(
children: [
("${LocaleKeys.transmission.tr()}: ").toText(fontSize: 12, color: MyColors.lightTextColor),
"${widget.adDetails.vehicle!.transmission!.label}".toText(fontSize: 12),
"${isArabic? widget.adDetails.vehicle!.transmission!.labelN :widget.adDetails.vehicle!.transmission!.label}".toText(fontSize: 12),
],
),
Row(
children: [
("${LocaleKeys.odometer.tr()}: ").toText(fontSize: 12, color: MyColors.lightTextColor),
"${widget.adDetails.vehicle!.odometer ?? 0} Km".toText(fontSize: 12),
"${widget.adDetails.vehicle!.odometer ?? 0} ${LocaleKeys.km.tr()}".toText(fontSize: 12),
],
),
Row(
@ -363,12 +386,26 @@ class _AdsDetailViewState extends State<AdsDetailView> {
Widget buildAdStartEndDates() {
String startDate = "";
String endDate = "";
bool isArabic = AppState().isArabic(context);
final atText = isArabic ? "الساعة" : "at";
if (widget.adDetails.startdate != null) {
startDate = "${DateHelper.formatAsDayMonthYear(DateTime.parse(widget.adDetails.startdate!))} at ${DateHelper.formatAsTime(DateTime.parse(widget.adDetails.startdate!))}";
// if (widget.adDetails.startdate != null) {
// startDate = "${DateHelper.formatAsDayMonthYear(DateTime.parse(widget.adDetails.startdate!))} at ${DateHelper.formatAsTime(DateTime.parse(widget.adDetails.startdate!))}";
// }
// if (widget.adDetails.enddate != null) {
// endDate = "${DateHelper.formatAsDayMonthYear(DateTime.parse(widget.adDetails.enddate!))} at ${DateHelper.formatAsTime(DateTime.parse(widget.adDetails.enddate!))}";
// }
if (widget.adDetails.startdate != null) {startDate = "${DateHelper.formatAsDayMonthYear(DateTime.parse(widget.adDetails.startdate!),
isArabic: isArabic,)} $atText ${DateHelper.formatAsTime(
DateTime.parse(widget.adDetails.startdate!),
isArabic: isArabic,
)}";
}
if (widget.adDetails.enddate != null) {
endDate = "${DateHelper.formatAsDayMonthYear(DateTime.parse(widget.adDetails.enddate!))} at ${DateHelper.formatAsTime(DateTime.parse(widget.adDetails.enddate!))}";
if (widget.adDetails.enddate != null) {endDate = "${DateHelper.formatAsDayMonthYear(DateTime.parse(widget.adDetails.enddate!),
isArabic: isArabic,)} $atText ${DateHelper.formatAsTime(
DateTime.parse(widget.adDetails.enddate!),
isArabic: isArabic,
)}";
}
return Column(
children: [
@ -526,3 +563,179 @@ class _AdsDetailViewState extends State<AdsDetailView> {
);
}
}
class FavoriteAdButton extends StatefulWidget {
final AdDetailsModel adDetails;
const FavoriteAdButton({super.key, required this.adDetails});
@override
State<FavoriteAdButton> createState() => _FavoriteAdButtonState();
}
class _FavoriteAdButtonState extends State<FavoriteAdButton> with SingleTickerProviderStateMixin {
bool isLoading = false;
late bool isFavorite;
late AnimationController _animationController;
late Animation<double> _scaleAnimation;
@override
void initState() {
super.initState();
isFavorite = widget.adDetails.isAdFavorite ?? false;
_animationController = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this,
);
_scaleAnimation = Tween<double>(begin: 1.0, end: 1.3).animate(
CurvedAnimation(parent: _animationController, curve: Curves.easeInOut),
);
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
Future<void> toggleFavorite() async {
if (isLoading) return;
setState(() {
isLoading = true;
});
_animationController.forward().then((_) => _animationController.reverse());
bool success = await context.read<AdVM>().toggleAdFavorite(
adsID: widget.adDetails.id!,
isFavorite: isFavorite,
);
if (success) {
setState(() {
isFavorite = !isFavorite;
widget.adDetails.isAdFavorite = isFavorite;
isLoading = false;
});
} else {
setState(() {
isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: toggleFavorite,
child: Container(
width: 8.6.w,
height: 8.6.w,
decoration: BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: const Color(0xff000000).withOpacity(.07),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Center(
child: isLoading
? SizedBox(
width: 4.4.w,
height: 4.4.w,
child: const CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(MyColors.darkPrimaryColor),
),
)
: ScaleTransition(
scale: _scaleAnimation,
child: Icon(
isFavorite ? Icons.favorite : Icons.favorite_border,
color: isFavorite ? MyColors.darkPrimaryColor : MyColors.darkIconColor,
size: 5.5.w,
),
),
),
),
);
}
}
class ShareAdButton extends StatelessWidget {
final AdDetailsModel adDetails;
const ShareAdButton({super.key, required this.adDetails});
void _shareAd() {
final vehicle = adDetails.vehicle!;
// Build share message
String shareMessage = '';
// Add header
shareMessage += '${LocaleKeys.checkOutThisAd.tr()}\n\n';
// Vehicle details
shareMessage += '${vehicle.vehicleTitle ?? ""}\n';
shareMessage += '${LocaleKeys.model.tr()}: ${vehicle.modelyear?.label ?? ""}\n';
shareMessage += '${LocaleKeys.mileage.tr()}: ${vehicle.mileage?.mileageEnd ?? ""}\n';
shareMessage += '${LocaleKeys.price.tr()}: ${vehicle.demandAmount?.toInt()} ${vehicle.currency ?? ""}\n';
if (vehicle.cityName != null && vehicle.cityName!.isNotEmpty) {
shareMessage += '${LocaleKeys.location.tr()}: ${vehicle.cityName}\n';
}
shareMessage += '\n';
// App download links
shareMessage += '${LocaleKeys.downloadApp.tr()}\n';
if (Platform.isAndroid) {
shareMessage += '${LocaleKeys.androidApp.tr()}: https://play.google.com/store/apps/details?id=com.mowater.app\n';
} else if (Platform.isIOS) {
shareMessage += '${LocaleKeys.iosApp.tr()}: https://apps.apple.com/app/mowater/id123456789\n';
} else {
// Include both if platform detection fails
shareMessage += '${LocaleKeys.androidApp.tr()}: https://play.google.com/store/apps/details?id=com.mowater.app\n';
shareMessage += '${LocaleKeys.iosApp.tr()}: https://apps.apple.com/app/mowater/id123456789\n';
}
// Share using share_plus
Share.share(shareMessage);
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: _shareAd,
child: Container(
width: 8.6.w,
height: 8.6.w,
decoration: BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: const Color(0xff000000).withOpacity(.07),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Center(
child: Icon(
Icons.share,
color: MyColors.darkIconColor,
size: 5.5.w,
),
),
),
);
}
}

@ -125,6 +125,8 @@ class AdDurationSelectionSheet extends StatelessWidget {
isBorderRequired: adDuration.id == adVM.vehicleAdDurationId.selectedId,
),
).onPress(() {
log("isFromExtendAd: $isFromExtendAd");
if (isFromExtendAd) {
adVM.updateVehicleExtendAdDurationId(
SelectionModel(

@ -1,4 +1,5 @@
import 'dart:developer';
import 'dart:ui';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
@ -17,6 +18,8 @@ import 'package:mc_common_app/view_models/ad_view_model.dart';
import 'package:mc_common_app/widgets/extensions/extensions_widget.dart';
import 'package:provider/provider.dart';
import '../../../classes/app_state.dart';
class AdsListWidget extends StatelessWidget {
final List<AdDetailsModel> adsList;
final ScrollPhysics? scrollPhysics;
@ -108,19 +111,54 @@ class AdsListWidget extends StatelessWidget {
}
}
class AdCard extends StatelessWidget {
class AdCard extends StatefulWidget {
final AdDetailsModel adDetails;
final bool isAdsFragment;
final bool shouldShowAdStatus;
final bool needBorder;
const AdCard({
super.key,
required this.adDetails,
required this.isAdsFragment,
required this.shouldShowAdStatus,
this.needBorder = true,
});
@override
State<AdCard> createState() => _AdCardState();
}
class _AdCardState extends State<AdCard> {
int _currentImageIndex = 0;
late PageController _pageController;
const AdCard({super.key, required this.adDetails, required this.isAdsFragment, required this.shouldShowAdStatus});
@override
void initState() {
super.initState();
_pageController = PageController();
}
@override
void dispose() {
_pageController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
bool isArabic = AppState().isArabic(context);
final bool hasMultipleImages = widget.adDetails.vehicle?.image?.length == null
? false
: (widget.adDetails.vehicle?.image?.length == 0 || widget.adDetails.vehicle?.image?.length == 1)
? false
: true;
final isAdsFragment = widget.isAdsFragment;
return Stack(
alignment: Alignment.center,
children: [
if (adDetails.vehicle == null) ...[
if (widget.adDetails.vehicle == null) ...[
Expanded(
child: Column(
children: [
@ -131,10 +169,97 @@ class AdCard extends StatelessWidget {
] else ...[
Row(
children: [
adDetails.vehicle!.image!.first.imageUrl.buildNetworkImage(
width: 80,
height: 80,
fit: BoxFit.cover,
Stack(
alignment: Alignment.center,
children: [
ClipRRect(
child: SizedBox(
width: 100,
height: 100,
child: hasMultipleImages
? PageView.builder(
controller: _pageController,
itemCount: widget.adDetails.vehicle!.image!.length,
onPageChanged: (index) {
setState(() {
_currentImageIndex = index;
});
},
itemBuilder: (context, index) {
return widget.adDetails.vehicle!.image![index].imageUrl.buildNetworkImage(
width: 100,
height: 100,
fit: BoxFit.cover,
);
},
)
: widget.adDetails.vehicle!.image!.first.imageUrl.buildNetworkImage(
width: 100,
height: 100,
fit: BoxFit.cover,
),
),
),
// Image indicator dots
if (hasMultipleImages)
Positioned(
bottom: 6,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.4),
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: List.generate(
widget.adDetails.vehicle!.image!.length,
(index) => Container(
width: 6,
height: 6,
margin: const EdgeInsets.symmetric(horizontal: 3),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: _currentImageIndex == index ? Colors.white : Colors.white.withOpacity(0.5),
border: Border.all(
color: Colors.black.withOpacity(0.2),
width: 0.5,
),
),
),
),
),
),
),
if (widget.adDetails.adPostStatus == AdPostStatus.sold) ...[
// Blur overlay
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 3, sigmaY: 3),
child: Container(
width: 100,
height: 100,
color: Colors.black.withOpacity(0.3),
),
),
),
// SOLD text
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.circular(4),
),
child: LocaleKeys.sold.tr().toText(
fontSize: 12,
color: Colors.white,
isBold: true,
),
),
],
],
),
12.width,
Expanded(
@ -150,12 +275,37 @@ class AdCard extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
if (isAdsFragment && adDetails.adPostStatus! == AdPostStatus.sold && adDetails.statuslabel != null && adDetails.statuslabel!.isNotEmpty) ...[
Utils.statusContainerChip(text: adDetails.statuslabel ?? "", chipColor: Utils.getChipColorByAdStatus(adDetails.adPostStatus!)),
] else if (isAdsFragment && !context.read<AdVM>().isExploreAdsTapped && adDetails.statuslabel != null && adDetails.statuslabel!.isNotEmpty) ...[
Utils.statusContainerChip(text: adDetails.statuslabel ?? "", chipColor: Utils.getChipColorByAdStatus(adDetails.adPostStatus!)),
// if (isAdsFragment && adDetails.adPostStatus! == AdPostStatus.sold && adDetails.statuslabel != null && adDetails.statuslabel!.isNotEmpty) ...[
// Utils.statusContainerChip(text: adDetails.statuslabel ?? "", chipColor: Utils.getChipColorByAdStatus(adDetails.adPostStatus!)),
// ] else if (isAdsFragment && !context.read<AdVM>().isExploreAdsTapped && adDetails.statuslabel != null && adDetails.statuslabel!.isNotEmpty) ...[
// Utils.statusContainerChip(text: adDetails.statuslabel ?? "", chipColor: Utils.getChipColorByAdStatus(adDetails.adPostStatus!)),
// ],
if (isAdsFragment &&
widget.adDetails.adPostStatus! == AdPostStatus.sold &&
widget.adDetails.statuslabel != null &&
widget.adDetails.statuslabel!.isNotEmpty) ...[
Utils.statusContainerChip(
text: isArabic
? (widget.adDetails.statuslabelN ?? "")
: (widget.adDetails.statuslabel ?? ""),
chipColor: Utils.getChipColorByAdStatus(widget.adDetails.adPostStatus!),
),
] else if (isAdsFragment &&
!context.read<AdVM>().isExploreAdsTapped &&
widget.adDetails.statuslabel != null &&
widget.adDetails.statuslabel!.isNotEmpty) ...[
Utils.statusContainerChip(
text: isArabic
? (widget.adDetails.statuslabelN ?? "")
: (widget.adDetails.statuslabel ?? ""),
chipColor: Utils.getChipColorByAdStatus(widget.adDetails.adPostStatus!),
),
],
// Show Accident Free chip for explore ads when isAccidentFree is true
if (isAdsFragment && context.read<AdVM>().isExploreAdsTapped && widget.adDetails.vehicle?.isAccidentFree == true) ...[
Utils.statusContainerChip(text: LocaleKeys.accidentFree.tr(), chipColor: Colors.green),
],
(adDetails.vehicle!.vehicleTitle ?? "").toText(
(widget.adDetails.vehicle!.vehicleTitle ?? "").toText(
fontSize: 16,
letterSpacing: -0.64,
height: 25 / 16,
@ -169,7 +319,7 @@ class AdCard extends StatelessWidget {
height: 18 / 12,
),
2.width,
(adDetails.vehicle!.modelyear!.label ?? "").toText(
(widget.adDetails.vehicle!.modelyear!.label ?? "").toText(
fontSize: 12,
letterSpacing: -0.48,
height: 18 / 12,
@ -185,7 +335,7 @@ class AdCard extends StatelessWidget {
height: 18 / 12,
),
2.width,
(adDetails.vehicle!.mileage!.mileageEnd ?? "").toText(
(widget.adDetails.vehicle!.mileage!.mileageEnd ?? "").toText(
fontSize: 12,
letterSpacing: -0.48,
height: 18 / 12,
@ -199,8 +349,16 @@ class AdCard extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.start,
children: [
(adDetails.vehicle!.cityName ?? "").toText(color: MyColors.lightTextColor),
adDetails.createdOn != null ? DateTime.parse(adDetails.createdOn!).getTimeAgo().toText(color: MyColors.lightTextColor) : const SizedBox(),
(isArabic? widget.adDetails.vehicle!.cityNameN ?? "" : widget.adDetails.vehicle!.cityName ?? "").toText(color: MyColors.lightTextColor),
// widget.adDetails.createdOn != null ? DateTime.parse(widget.adDetails.createdOn!).getTimeAgo().toText(color: MyColors.lightTextColor) : const SizedBox(),
widget.adDetails.createdOn != null
? DateFormat(
'd MMM, y',
isArabic ? 'ar' : 'en',
)
.format(DateTime.parse(widget.adDetails.createdOn!))
.toText(color: MyColors.lightTextColor)
: const SizedBox(),
],
),
],
@ -213,9 +371,9 @@ class AdCard extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
(adDetails.vehicle!.demandAmount!.toInt().toString()).toText(fontSize: 19, isBold: true, letterSpacing: -1.16, height: 29 / 19),
(widget.adDetails.vehicle!.demandAmount!.toInt().toString()).toText(fontSize: 19, isBold: true, letterSpacing: -1.16, height: 29 / 19),
2.width,
(adDetails.vehicle!.currency ?? "").toText(color: MyColors.lightTextColor, fontSize: 10, letterSpacing: -0.4, height: 16 / 10).paddingOnly(bottom: 3),
(widget.adDetails.vehicle!.currency ?? "").toText(color: MyColors.lightTextColor, fontSize: 10, letterSpacing: -0.4, height: 16 / 10).paddingOnly(bottom: 3),
],
),
),
@ -258,6 +416,7 @@ class AdCard extends StatelessWidget {
).toWhiteContainer(
width: double.infinity,
allPading: 12,
isBorderRequired: widget.needBorder,
);
}
}

@ -1,5 +1,3 @@
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:mc_common_app/classes/app_state.dart';
import 'package:mc_common_app/extensions/int_extensions.dart';
@ -7,7 +5,6 @@ import 'package:mc_common_app/generated/locale_keys.g.dart';
import 'package:mc_common_app/theme/colors.dart';
import 'package:mc_common_app/utils/enums.dart';
import 'package:mc_common_app/utils/navigator.dart';
import 'package:mc_common_app/utils/utils.dart';
import 'package:mc_common_app/view_models/ad_view_model.dart';
import 'package:mc_common_app/views/advertisement/ad_creation_steps/ad_duration_container.dart';
import 'package:mc_common_app/views/advertisement/ad_creation_steps/ad_review_containers.dart';
@ -76,18 +73,45 @@ class BuildFooterButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
final isArabic = AppState().isArabic(context);
return Consumer(
builder: (BuildContext context, AdVM adVm, Widget? child) {
switch (adVm.currentProgressStep) {
case AdCreationStepsEnum.vehicleDetails:
return SizedBox(
width: double.infinity,
child: ShowFillButton(
title: LocaleKeys.next.tr(),
onPressed: () {
adVm.updateCurrentStep(context);
},
),
return Row(
children: [
Expanded(
child: ShowFillButton(
txtColor: MyColors.black,
maxHeight: 55,
// title: "Save As Draft",
title: isArabic ? "حفظ كمسودة" : "Save As Draft",
onPressed: () async {
await adVm.saveAdAsDraft(context: context, stepNoEnum: AdCreationStepsEnum.vehicleDetails, bypassValidation: true);
adVm.isDraftEditEnabled = false;
adVm.isAdEditEnabled = false;
adVm.currentProgressStep = AdCreationStepsEnum.vehicleDetails;
adVm.resetValues();
pop(context);
if (AppState().currentAppType == AppType.customer) {
pop(context);
}
},
backgroundColor: MyColors.greyButtonColor,
),
),
12.width,
Expanded(
child: ShowFillButton(
maxHeight: 55,
title: LocaleKeys.next.tr(),
onPressed: () {
adVm.updateCurrentStep(context);
},
backgroundColor: MyColors.darkPrimaryColor,
),
),
],
);
case AdCreationStepsEnum.damageParts:
return Row(
@ -96,16 +120,18 @@ class BuildFooterButton extends StatelessWidget {
child: ShowFillButton(
txtColor: MyColors.black,
maxHeight: 55,
title: LocaleKeys.cancel.tr(),
onPressed: () {
// title: "Save As Draft",
title: isArabic ? "حفظ كمسودة" : "Save As Draft",
onPressed: () async {
await adVm.saveAdAsDraft(context: context, stepNoEnum: AdCreationStepsEnum.damageParts, bypassValidation: true);
adVm.isDraftEditEnabled = false;
adVm.isAdEditEnabled = false;
adVm.currentProgressStep = AdCreationStepsEnum.vehicleDetails;
adVm.resetValues();
pop(context);
if (AppState().currentAppType == AppType.customer) {
pop(context);
}
Utils.showToast('Saved as Draft');
adVm.getMyDraftAds();
},
backgroundColor: MyColors.greyButtonColor,
),
@ -130,17 +156,18 @@ class BuildFooterButton extends StatelessWidget {
child: ShowFillButton(
txtColor: MyColors.black,
maxHeight: 55,
title: LocaleKeys.cancel.tr(),
onPressed: () {
// title: "Save As Draft",
title: isArabic ? "حفظ كمسودة" : "Save As Draft",
onPressed: () async {
await adVm.saveAdAsDraft(context: context, stepNoEnum: AdCreationStepsEnum.adDuration, bypassValidation: true);
adVm.isDraftEditEnabled = false;
adVm.isAdEditEnabled = false;
adVm.currentProgressStep = AdCreationStepsEnum.vehicleDetails;
adVm.resetValues();
pop(context);
if (AppState().currentAppType == AppType.customer) {
pop(context);
}
Utils.showToast('Saved as Draft');
adVm.getMyDraftAds();
},
backgroundColor: MyColors.greyButtonColor,
),
@ -165,17 +192,18 @@ class BuildFooterButton extends StatelessWidget {
child: ShowFillButton(
txtColor: MyColors.black,
maxHeight: 55,
title: LocaleKeys.cancel.tr(),
onPressed: () {
//title: "Save As Draft",
title: isArabic ? "حفظ كمسودة" : "Save As Draft",
onPressed: () async {
await adVm.saveAdAsDraft(context: context, stepNoEnum: AdCreationStepsEnum.reviewAd, bypassValidation: true);
adVm.isDraftEditEnabled = false;
adVm.isAdEditEnabled = false;
adVm.currentProgressStep = AdCreationStepsEnum.vehicleDetails;
adVm.resetValues();
pop(context);
if (AppState().currentAppType == AppType.customer) {
pop(context);
}
Utils.showToast('Saved as Draft');
adVm.getMyDraftAds();
},
backgroundColor: MyColors.greyButtonColor,
),

@ -0,0 +1,104 @@
import 'dart:async';
import 'package:mc_common_app/generated/locale_keys.g.dart';
import 'package:mc_common_app/models/advertisment_models/ad_details_model.dart';
import 'package:mc_common_app/view_models/ad_view_model.dart';
import 'package:flutter/material.dart';
import 'package:mc_common_app/extensions/int_extensions.dart';
import 'package:mc_common_app/utils/enums.dart';
import 'package:mc_common_app/views/advertisement/components/ads_list_widget.dart';
import 'package:mc_common_app/widgets/common_widgets/app_bar.dart';
import 'package:mc_common_app/widgets/common_widgets/filters_list.dart';
import 'package:mc_common_app/widgets/extensions/extensions_widget.dart';
import 'package:provider/provider.dart';
import 'package:easy_localization/easy_localization.dart';
class FavouriteAdsView extends StatefulWidget {
const FavouriteAdsView({super.key});
@override
State<FavouriteAdsView> createState() => _FavouriteAdsViewState();
}
class _FavouriteAdsViewState extends State<FavouriteAdsView> {
int selectedBrandId = 0;
@override
void initState() {
scheduleMicrotask(() async {
final adVM = context.read<AdVM>();
await adVM.getFavouriteAds();
// Load filter options if not already loaded
if (adVM.exploreAdsFilterOptions.isEmpty) {
await adVM.populateAdsFilterList();
}
});
super.initState();
}
List<AdDetailsModel> getFilteredAds(AdVM adVM) {
if (selectedBrandId == 0) {
return adVM.favouriteAds;
}
return adVM.favouriteAds.where((ad) => ad.vehicle?.model?.vehicleBrandID == selectedBrandId).toList();
}
@override
Widget build(BuildContext context) {
return Consumer(builder: (BuildContext context, AdVM adVM, Widget? child) {
return Scaffold(
appBar: CustomAppBar(title: LocaleKeys.myFavouriteAds.tr()),
body: SizedBox(
width: double.infinity,
height: double.infinity,
child: Column(
children: [
if (adVM.exploreAdsFilterOptions.isNotEmpty) ...[
16.height,
FiltersList(
showImages: true,
filterList: adVM.exploreAdsFilterOptions,
onFilterTapped: (index, selectedFilterId) {
setState(() {
selectedBrandId = selectedFilterId;
});
// Update filter selection in the list
for (var filter in adVM.exploreAdsFilterOptions) {
filter.isSelected = false;
}
adVM.exploreAdsFilterOptions[index].isSelected = true;
},
needLeftPadding: false,
).paddingOnly(left: 21),
],
16.height,
Expanded(
child: RefreshIndicator(
onRefresh: () async {
await adVM.getFavouriteAds();
},
child: adVM.state == ViewState.busy
? const Center(child: CircularProgressIndicator())
: getFilteredAds(adVM).isEmpty
? Center(
child: Text(
LocaleKeys.noFavouriteAds.tr(),
style: const TextStyle(fontSize: 16),
),
)
: AdsListWidget(
isAdsFragment: false,
isDraftAds: false,
shouldShowAdStatus: false,
adsList: getFilteredAds(adVM),
hasMoreData: false,
),
),
),
],
),
),
);
});
}
}

@ -70,16 +70,16 @@ class SelectAdTypeView extends StatelessWidget {
children: [
Expanded(
child: Consumer(builder: (BuildContext context, AdVM adVM, Widget? child) {
final isArabic = AppState().isArabic(context);
return ListView.separated(
itemCount: adVM.vehicleTypes.length,
itemBuilder: (BuildContext context, int index) {
VehicleTypeModel vehicleTypeModel = adVM.vehicleTypes[index];
return InkWell(
onTap: () async {
adVM.updateSelectionVehicleTypeId(SelectionModel(selectedId: vehicleTypeModel.id!, selectedOption: vehicleTypeModel.vehicleTypeName ?? "", errorValue: ""));
adVM.updateSelectionVehicleTypeId(SelectionModel(selectedId: vehicleTypeModel.id!, selectedOption: isArabic ? vehicleTypeModel.vehicleTypeNameN ?? "" : vehicleTypeModel.vehicleTypeName ?? "", errorValue: ""));
if (adVM.isAdEditEnabled) {
adVM.
autoFillSelectedVehicleAdsDetails();
adVM.autoFillSelectedVehicleAdsDetails(context);
}
if (AppState().currentAppType == AppType.provider) {
if (isFromExtendAd && !adVM.isAdEditEnabled) {
@ -114,7 +114,9 @@ class SelectAdTypeView extends StatelessWidget {
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
vehicleTypeModel.vehicleTypeName.toString().toText(fontSize: 18),
isArabic ? vehicleTypeModel.vehicleTypeNameN.toString().toText(fontSize: 18)
: vehicleTypeModel.vehicleTypeName.toString().toText(fontSize: 18),
// vehicleTypeModel.vehicleTypeName.toString().toText(fontSize: 18),
getVehicleAdTypeIcon(vehicleTypeModel.vehicleTypeEnum!),
],
),

@ -22,6 +22,8 @@ import 'package:mc_common_app/widgets/extensions/extensions_widget.dart';
import 'package:mc_common_app/widgets/txt_field.dart';
import 'package:provider/provider.dart';
import '../../../classes/app_state.dart';
class AppointmentServicePickBottomSheet extends StatelessWidget {
const AppointmentServicePickBottomSheet({super.key});
@ -70,12 +72,16 @@ class AppointmentServicePickBottomSheet extends StatelessWidget {
8.height,
Builder(
builder: (context) {
bool isArabic = AppState().isArabic(context);
List<DropValue> serviceCategories = [];
for (var element in appointmentsVM.branchServices) {
if (element.categoryId == appointmentsVM.branchSelectedCategoryId.selectedId) {
serviceCategories.add(DropValue(
element.serviceProviderServiceId ?? 0,
element.serviceDescription!,
isArabic
? element.serviceDescriptionN!
: element.serviceDescription!,
// element.serviceDescription!,
"",
));
}

@ -173,7 +173,7 @@ class AppointmentHomeTileWidget extends StatelessWidget {
children: [
!isForProvider ? MyAssets.miniClock.buildSvg(height: 12) : const SizedBox(),
2.width,
"${isForProvider ? "Appt. on:" : ""} ${appointmentListModel!.duration ?? ""} ${appointmentListModel!.appointmentDate!.toFormattedDateWithoutTime()}"
"${isForProvider ? LocaleKeys.apptOn.tr() : ""} ${appointmentListModel!.duration ?? ""} ${appointmentListModel!.appointmentDate!.toFormattedDateWithoutTime()}"
.toText(color: MyColors.lightTextColor, fontSize: 12, letterSpacing: -0.48, height: 19 / 12),
],
),
@ -291,7 +291,7 @@ class AppointmentFragmentTileWidget extends StatelessWidget {
(appointmentListModel!.providerName ?? "").toText(color: MyColors.darkTextColor, fontSize: 16, letterSpacing: -0.64, height: 24 / 16),
Row(
children: [
('Branch:').toText(color: MyColors.greyColor, fontSize: 12, letterSpacing: -0.48, height: 18 / 12),
("${LocaleKeys.branch.tr()}:").toText(color: MyColors.greyColor, fontSize: 12, letterSpacing: -0.48, height: 18 / 12),
2.width,
(appointmentListModel!.branchName ?? "").toText(color: MyColors.darkTextColor, fontSize: 12, letterSpacing: -0.48, height: 18 / 12),
],

@ -20,6 +20,7 @@ import 'package:mc_common_app/widgets/button/show_fill_button.dart';
import 'package:mc_common_app/widgets/common_widgets/app_bar.dart';
import 'package:mc_common_app/widgets/common_widgets/filters_list.dart';
import 'package:mc_common_app/widgets/extensions/extensions_widget.dart';
import 'package:mc_common_app/widgets/shimmer/home_shimmer_widgets.dart';
import 'package:provider/provider.dart';
class AdsFragment extends StatelessWidget {
@ -147,7 +148,7 @@ class AdsFragment extends StatelessWidget {
}
},
child: adVM.state == ViewState.busy
? const Center(child: CircularProgressIndicator())
? const AdsListShimmer(itemCount: 5)
: AdsListWidget(
isAdsFragment: true,
isDraftAds: false,

@ -19,6 +19,7 @@ import 'package:mc_common_app/widgets/button/show_fill_button.dart';
import 'package:mc_common_app/widgets/common_widgets/app_bar.dart';
import 'package:mc_common_app/widgets/common_widgets/filters_list.dart';
import 'package:mc_common_app/widgets/extensions/extensions_widget.dart';
import 'package:mc_common_app/widgets/shimmer/home_shimmer_widgets.dart';
import 'package:provider/provider.dart';
import 'package:easy_localization/easy_localization.dart';
@ -124,12 +125,22 @@ class MyRequestsFragment extends StatelessWidget {
requestsVM.applyFilterOnRequestsVM(requestsTypeEnum: selectedFilterId.toRequestTypeEnum());
},
),
// Show condition filters for all request types
if (requestsVM.sparePartsConditionFilterOptions.isNotEmpty) ...[
8.height,
FiltersList(
filterList: requestsVM.sparePartsConditionFilterOptions,
onFilterTapped: (index, selectedFilterId) {
requestsVM.applySparePartsConditionFilter(conditionId: selectedFilterId);
},
),
],
8.height,
Expanded(
child: RefreshIndicator(
onRefresh: () async => await requestsVM.getRequestsBasedOnFilters(),
child: requestsVM.state == ViewState.busy
? const Center(child: CircularProgressIndicator())
? const RequestsListShimmer(itemCount: 5)
: requestsVM.myFilteredRequests.isEmpty
? Column(
mainAxisAlignment: MainAxisAlignment.center,

@ -1,3 +1,4 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:mc_common_app/classes/consts.dart';
import 'package:mc_common_app/extensions/int_extensions.dart';
@ -12,73 +13,56 @@ import 'package:mc_common_app/widgets/common_widgets/app_bar.dart';
import 'package:provider/provider.dart';
import 'package:easy_localization/easy_localization.dart';
class PaymentMethodsView extends StatelessWidget {
class PaymentMethodsView extends StatefulWidget {
final PaymentTypes paymentType;
final Function? onTempContinue;
const PaymentMethodsView({Key? key, required this.paymentType, this.onTempContinue}) : super(key: key);
@override
State<PaymentMethodsView> createState() => _PaymentMethodsViewState();
}
class _PaymentMethodsViewState extends State<PaymentMethodsView> {
@override
void initState() {
super.initState();
// Automatically trigger payment flow when screen opens
scheduleMicrotask(() async {
// Show loading for 2 seconds so user can see it
await Future.delayed(const Duration(seconds: 2));
bool isNeedToCallPayment = true;
if (widget.onTempContinue != null) {
isNeedToCallPayment = await widget.onTempContinue!();
}
// Pop the loading screen BEFORE calling payment to avoid context issues
if (mounted) {
Navigator.of(context).pop();
}
if (isNeedToCallPayment && mounted) {
// Call payment which will open webview (context still works for PaymentVM)
await context.read<PaymentVM>().onVisaCardSelected(context, widget.paymentType);
}
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: CustomAppBar(
title: LocaleKeys.paymentMethod.tr(),
profileImageUrl: MyAssets.bnCar,
isRemoveBackButton: false,
isDrawerEnabled: false,
),
body: Container(
padding: const EdgeInsets.only(bottom: 10, left: 21, right: 21),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
20.height,
LocaleKeys.selectPaymentMethod.tr().toText(fontSize: 18, isBold: true),
15.height,
Consumer(
builder: (BuildContext context, PaymentVM paymentVm, Widget? child) {
return Expanded(
child: ListView(
shrinkWrap: true,
children: [
PaymentMethodContainer(
isSelected: paymentVm.selectedPaymentMethod == PaymentMethods.mada, onTap: () => paymentVm.updateSelectedPaymentMethod(PaymentMethods.mada), cardAsset: MyAssets.madaPng),
PaymentMethodContainer(
isSelected: paymentVm.selectedPaymentMethod == PaymentMethods.visa, onTap: () => paymentVm.updateSelectedPaymentMethod(PaymentMethods.visa), cardAsset: MyAssets.visaPng),
PaymentMethodContainer(
isSelected: paymentVm.selectedPaymentMethod == PaymentMethods.applePay,
onTap: () => paymentVm.updateSelectedPaymentMethod(PaymentMethods.applePay),
cardAsset: MyAssets.applePayPng),
PaymentMethodContainer(
isSelected: paymentVm.selectedPaymentMethod == PaymentMethods.masterCard,
onTap: () => paymentVm.updateSelectedPaymentMethod(PaymentMethods.masterCard),
cardAsset: MyAssets.mastercardPng),
PaymentMethodContainer(
isSelected: paymentVm.selectedPaymentMethod == PaymentMethods.tamara,
onTap: () => paymentVm.updateSelectedPaymentMethod(PaymentMethods.tamara),
cardAsset: MyAssets.tamaraEngPng),
],
),
);
},
const CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(MyColors.darkPrimaryColor),
),
SizedBox(
width: double.infinity,
child: ShowFillButton(
maxHeight: 55,
title: LocaleKeys.continu.tr(),
onPressed: () async {
bool isNeedToCallPayment = true;
if (onTempContinue != null) {
isNeedToCallPayment = await onTempContinue!();
}
if (isNeedToCallPayment) {
context.read<PaymentVM>().onContinuePressed(context, paymentType: paymentType);
}
},
backgroundColor: MyColors.darkPrimaryColor,
),
24.height,
LocaleKeys.pleaseWait.tr().toText(
fontSize: 16,
color: MyColors.darkTextColor,
),
],
),

@ -88,9 +88,13 @@ class CreateRequestPage extends StatelessWidget {
),
] else ...[
Builder(builder: (context) {
bool isArabic = AppState().isArabic(context);
List<DropValue> vehicleTypeDrop = [];
for (var element in requestsVM.vehicleTypes) {
vehicleTypeDrop.add(DropValue(element.id?.toInt() ?? 0, element.vehicleTypeName ?? "", ""));
vehicleTypeDrop.add(DropValue(element.id?.toInt() ?? 0,
isArabic
? element.vehicleTypeNameN ?? ""
: element.vehicleTypeName ?? "", ""));
}
return DropdownField(
(DropValue value) => requestsVM.updateSelectionVehicleTypeId(SelectionModel(selectedId: value.id, selectedOption: value.value)),
@ -155,9 +159,13 @@ class CreateRequestPage extends StatelessWidget {
),
8.height,
Builder(builder: (context) {
bool isArabic = AppState().isArabic(context);
List<DropValue> vehicleCountriesDrop = [];
for (var element in requestsVM.vehicleCountries) {
vehicleCountriesDrop.add(DropValue(element.id?.toInt() ?? 0, element.countryName ?? "", ""));
vehicleCountriesDrop.add(DropValue(element.id?.toInt() ?? 0,
isArabic
? element.countryNameN ?? ""
: element.countryName ?? "", ""));
}
return DropdownField(
(DropValue value) => requestsVM.updateSelectionVehicleCountryId(SelectionModel(selectedOption: value.value, selectedId: value.id)),
@ -177,9 +185,12 @@ class CreateRequestPage extends StatelessWidget {
] else ...[
8.height,
Builder(builder: (context) {
bool isArabic = AppState().isArabic(context);
List<DropValue> vehicleCitiesDrop = [];
for (var element in requestsVM.vehicleCities) {
vehicleCitiesDrop.add(DropValue(element.id?.toInt() ?? 0, element.cityName ?? "", ""));
vehicleCitiesDrop.add(DropValue(element.id?.toInt() ?? 0, isArabic
? element.cityNameN ?? ""
: element.cityName ?? "", ""));
}
return DropdownField(
(DropValue value) => requestsVM.updateSelectionVehicleCityId(SelectionModel(selectedId: value.id, selectedOption: value.value)),

@ -29,6 +29,7 @@ class RequestDetailPage extends StatelessWidget {
const RequestDetailPage({super.key, required this.requestDetailPageArguments});
Widget buildRequestDetailsContainer(BuildContext context) {
bool isArabic = AppState().isArabic(context);
final requestDetail = requestDetailPageArguments.requestModel;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
@ -42,7 +43,7 @@ class RequestDetailPage extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
requestDetail.vehicleTypeName.toText(fontSize: 16, letterSpacing: -0.64),
isArabic ? requestDetail.vehicleTypeNameN.toText(fontSize: 16, letterSpacing: -0.64) : requestDetail.vehicleTypeName.toText(fontSize: 16, letterSpacing: -0.64),
showItem("${LocaleKeys.brand.tr()}: ", requestDetail.brand),
showItem("${LocaleKeys.model.tr()}: ", requestDetail.model),
showItem("${LocaleKeys.year.tr()}: ", "${requestDetail.year}"),
@ -53,13 +54,21 @@ class RequestDetailPage extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.start,
children: [
"${requestDetail.city ?? ""} ${requestDetail.countryName}".toText(
"${isArabic ? requestDetail.cityNameN : requestDetail.city} "
"${isArabic ? requestDetail.countryNameN : requestDetail.countryName}"
.toText(
color: MyColors.lightTextColor,
),
// "${requestDetail.city ?? ""} ${requestDetail.countryName}".toText(
// color: MyColors.lightTextColor,
// ),
if (requestDetail.createdOn != null) ...[
DateTime.parse(requestDetail.createdOn!).getTimeAgo().toText(
color: MyColors.lightTextColor,
),
// DateTime.parse(requestDetail.createdOn!).getTimeAgo().toText(
// color: MyColors.lightTextColor,
// ),
DateTime.parse(requestDetail.createdOn!).getTimeAgo(isArabic: isArabic)
.toText(color: MyColors.lightTextColor,
),
],
],
),

@ -21,6 +21,7 @@ class RequestItem extends StatelessWidget {
@override
Widget build(BuildContext context) {
final isArabic = AppState().isArabic(context);
String imageUrl = "";
if (request.requestImages != null && request.requestImages!.isNotEmpty) {
imageUrl = request.requestImages!.first.imageUrl ?? "";
@ -42,9 +43,20 @@ class RequestItem extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (shouldShowStatuses) ...[
// Utils.statusContainerChip(
// text: request.requestStatusName.toLowerCase() == "InProgress".toLowerCase() ? "In Progress" : request.requestStatusName,
// chipColor: Utils.getChipColorByRequestStatus(request.requestStatus)),
Utils.statusContainerChip(
text: request.requestStatusName.toLowerCase() == "InProgress".toLowerCase() ? "In Progress" : request.requestStatusName,
chipColor: Utils.getChipColorByRequestStatus(request.requestStatus)),
text: isArabic
? (request.requestStatusNameN?.isNotEmpty == true
? request.requestStatusNameN!
: request.requestStatusName)
: (request.requestStatusName.toLowerCase() == "inprogress"
? "In Progress"
: request.requestStatusName),
chipColor:
Utils.getChipColorByRequestStatus(request.requestStatus),
),
],
6.height,
"${request.brand} ${request.model} | ${request.id}".toText(fontSize: 16, letterSpacing: -0.64),
@ -61,7 +73,7 @@ class RequestItem extends StatelessWidget {
showItem("${LocaleKeys.year.tr()}:", "${request.year}"),
],
if (request.vehicleTypeName.isNotEmpty) ...[
showItem("${LocaleKeys.vehicleType.tr()}:", request.vehicleTypeName),
showItem("${LocaleKeys.vehicleType.tr()}:", isArabic ? request.vehicleTypeNameN :request.vehicleTypeName),
],
showItem("${LocaleKeys.description.tr()}:", request.description),
],
@ -86,9 +98,21 @@ class RequestItem extends StatelessWidget {
),
],
2.height,
request.cityName.toText(color: MyColors.lightTextColor, letterSpacing: -0.4, fontSize: 10),
// request.cityName.toText(color: MyColors.lightTextColor, letterSpacing: -0.4, fontSize: 10),
// if (request.createdOn != null) ...[
// DateTime.parse(request.createdOn!).getTimeAgo().toText(color: MyColors.lightTextColor, letterSpacing: -0.4, fontSize: 10),
// ],
(isArabic
? (request.cityNameN?.isNotEmpty == true ? request.cityNameN! : request.cityName) : request.cityName).toText(color: MyColors.lightTextColor,
letterSpacing: -0.4,
fontSize: 10,
),
if (request.createdOn != null) ...[
DateTime.parse(request.createdOn!).getTimeAgo().toText(color: MyColors.lightTextColor, letterSpacing: -0.4, fontSize: 10),
DateTime.parse(request.createdOn!).getTimeAgo(isArabic: isArabic).toText(
color: MyColors.lightTextColor,
letterSpacing: -0.4,
fontSize: 10,
),
],
],
)

@ -14,6 +14,8 @@ import 'package:mc_common_app/widgets/extensions/extensions_widget.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:provider/provider.dart';
import '../../classes/app_state.dart';
class SettingOptionsFAQs extends StatefulWidget {
const SettingOptionsFAQs({super.key});
@ -31,6 +33,7 @@ class _SettingOptionsFAQsState extends State<SettingOptionsFAQs> {
await settingsOptionsVM.getAllFaqs();
});
super.initState();
}
@override
@ -44,6 +47,7 @@ class _SettingOptionsFAQsState extends State<SettingOptionsFAQs> {
),
body: Consumer(
builder: (BuildContext context, SettingOptionsVM settingsOptionsVM, Widget? child) {
bool isArabic = AppState().isArabic(context);
return Container(
color: MyColors.backgroundColor,
width: double.infinity,
@ -69,7 +73,7 @@ class _SettingOptionsFAQsState extends State<SettingOptionsFAQs> {
return ExpansionTile(
tilePadding: EdgeInsets.zero,
shape: const Border(),
title: (faqModel.question ?? "").toText(fontSize: 16).paddingOnly(left: 8, right: 8),
title: (isArabic ? faqModel.questionN ?? "" : faqModel.question ?? "").toText(fontSize: 16).paddingOnly(left: 8, right: 8),
onExpansionChanged: (value) {
setState(() {
settingsOptionsVM.faqsList[index].isCollapsed = value;
@ -85,7 +89,7 @@ class _SettingOptionsFAQsState extends State<SettingOptionsFAQs> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
5.height,
(faqModel.answer ?? "").toText(color: MyColors.lightTextColor, fontSize: 14, fontWeight: MyFonts.Medium),
(isArabic ? faqModel.answerN ?? "" : faqModel.answer ?? "").toText(color: MyColors.lightTextColor, fontSize: 14, fontWeight: MyFonts.Medium),
10.height,
],
).paddingOnly(left: 8, right: 8)

@ -83,9 +83,9 @@ class _SettingOptionsMoreState extends State<SettingOptionsMore> {
if (AppState().currentAppType == AppType.customer) ...[
CustomSettingOptionsTile(
leadingWidget: const Icon(Icons.favorite, size: 20),
titleText: LocaleKeys.favoriteList.tr(),
titleText: LocaleKeys.myFavouriteProviders.tr(),
needBorderBelow: true,
onTap: () => navigateWithName(context, AppRoutes.favoriteListView),
onTap: () => navigateWithName(context, AppRoutes.favoriteProvidersView),
),
] else ...[
CustomSettingOptionsTile(
@ -112,6 +112,12 @@ class _SettingOptionsMoreState extends State<SettingOptionsMore> {
navigateWithName(context, AppRoutes.loginWithPassword, arguments: false);
}),
],
CustomSettingOptionsTile(
leadingWidget: const Icon(Icons.favorite_border, size: 20),
titleText: LocaleKeys.myFavouriteAds.tr(),
needBorderBelow: true,
onTap: () => navigateWithName(context, AppRoutes.favouriteAdsView),
),
CustomSettingOptionsTile(
leadingWidget: SvgPicture.asset(
MyAssets.icAds,
@ -128,11 +134,25 @@ class _SettingOptionsMoreState extends State<SettingOptionsMore> {
CustomSettingOptionsTile(
leadingWidget: const Icon(Icons.settings, size: 20),
titleText: LocaleKeys.settings.tr(),
needBorderBelow: false,
needBorderBelow: true,
onTap: () {
// context.read<SettingOptionsVM>().getAppInvitationHistoryList();
navigateWithName(context, AppRoutes.settingOptionsInviteFriends);
}),
/////////////////////////////////me add to test
CustomSettingOptionsTile(
leadingWidget: const Icon(Icons.translate, size: 20),
titleText: LocaleKeys.language.tr(),
isForLanguage: true,
onTap: () {
context.read<UserVM>().changeLanguage(context);
Future.delayed(const Duration(seconds: 1), () {
setState(() {});
});
},
),
//////////////////////////////////////////////////////////
],
).toWhiteContainer(width: double.infinity, pading: const EdgeInsets.all(12), borderRadius: 0),
10.height,

@ -7,6 +7,8 @@ import 'package:mc_common_app/generated/locale_keys.g.dart';
import 'package:mc_common_app/theme/colors.dart';
import 'package:mc_common_app/widgets/extensions/extensions_widget.dart';
import '../../../classes/app_state.dart';
class CustomSettingOptionsTile extends StatelessWidget {
final Widget leadingWidget;
final String titleText;
@ -31,6 +33,7 @@ class CustomSettingOptionsTile extends StatelessWidget {
@override
Widget build(BuildContext context) {
final isArabic = AppState().isArabic(context);
return Column(
children: [
Row(
@ -56,10 +59,20 @@ class CustomSettingOptionsTile extends StatelessWidget {
],
),
isForLanguage
? LocaleKeys.english.tr().toText(fontSize: 12, isUnderLine: true, color: MyColors.primaryColor)
? (isArabic ? "English": "عربي")
.toText(
fontSize: 12,
isUnderLine: true,
color: MyColors.primaryColor,
)
: showTrailingArrow
? const Icon(Icons.arrow_forward, size: 18)
: const SizedBox()
? const Icon(Icons.arrow_forward, size: 18)
: const SizedBox(),
// isForLanguage
// ? LocaleKeys.english.tr().toText(fontSize: 12, isUnderLine: true, color: MyColors.primaryColor)
// : showTrailingArrow
// ? const Icon(Icons.arrow_forward, size: 18)
// : const SizedBox()
],
).onPress(onTap),
subTitle != null ? 3.height : 5.height,

@ -15,11 +15,31 @@ class SplashPage extends StatefulWidget {
}
class SplashPageState extends State<SplashPage> with WidgetsBindingObserver {
bool showFlashingLights = false;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
// performTimer(context);
startSplashTimer();
}
void startSplashTimer() {
// After 3 seconds, show the flashing lights gif
Future.delayed(const Duration(seconds: 3), () {
if (mounted) {
setState(() {
showFlashingLights = true;
});
}
});
// After 5 seconds total, navigate to next page
Future.delayed(const Duration(seconds: 5), () {
if (mounted) {
navigateToNextDeciderPage();
}
});
}
@override
@ -33,7 +53,7 @@ class SplashPageState extends State<SplashPage> with WidgetsBindingObserver {
super.didChangeAppLifecycleState(state);
if (state == AppLifecycleState.resumed) {
final chatVM = context.read<ChatVM>();
chatVM.buildHubConnection(context);
chatVM.buildHubConnection(context: context);
}
}
@ -68,12 +88,22 @@ class SplashPageState extends State<SplashPage> with WidgetsBindingObserver {
// Car Image (flex value adjusted to make it more responsive)
Expanded(
flex: 10,
child: Image.asset(
MyAssets.bnCar,
fit: BoxFit.cover,
width: double.infinity,
height: double.infinity, // Use full height in this expanded section
),
child: showFlashingLights
? Image.asset(
MyAssets.bnCarFlashingLights,
fit: BoxFit.contain,
width: double.infinity,
height: double.infinity,
)
: Transform.scale(
scale: 1.72, // Scale down to match gif size (1144/1920 0.6)
child: Image.asset(
MyAssets.bnCar,
fit: BoxFit.contain,
width: double.infinity,
height: double.infinity,
),
),
),
// Engine Image
Expanded(

@ -77,7 +77,7 @@ class _EditAccountPageState extends State<EditAccountPage> {
children: [
CustomSettingOptionsTile(
leadingWidget: const Icon(Icons.quickreply_outlined, size: 20),
titleText: "Requests",
titleText: LocaleKeys.requests.tr(),
needBorderBelow: true,
onTap: () {
context.read<DashboardVmCustomer>().onNavbarTapped(4);
@ -85,19 +85,19 @@ class _EditAccountPageState extends State<EditAccountPage> {
}),
CustomSettingOptionsTile(
leadingWidget: const Icon(Icons.settings, size: 20),
titleText: "Settings",
titleText: LocaleKeys.settings.tr(),
needBorderBelow: true,
onTap: () => navigateWithName(context, AppRoutes.settingOptionsInviteFriends),
),
CustomSettingOptionsTile(
leadingWidget: const Icon(Icons.question_mark_outlined, size: 20),
titleText: "Help",
titleText: LocaleKeys.help.tr(),
needBorderBelow: true,
onTap: () => navigateWithName(context, AppRoutes.settingOptionsInviteFriends),
),
CustomSettingOptionsTile(
leadingWidget: const Icon(Icons.person, size: 20),
titleText: "Account",
titleText: LocaleKeys.account.tr(),
needBorderBelow: false,
onTap: () {
context.read<DashboardVmCustomer>().onNavbarTapped(4);
@ -108,7 +108,7 @@ class _EditAccountPageState extends State<EditAccountPage> {
10.height,
CustomSettingOptionsTile(
leadingWidget: const Icon(Icons.translate, size: 20),
titleText: "Language",
titleText: LocaleKeys.language.tr(),
isForLanguage: true,
onTap: () {},
).toWhiteContainer(width: double.infinity, pading: const EdgeInsets.all(12), borderRadius: 0),
@ -124,8 +124,8 @@ class _EditAccountPageState extends State<EditAccountPage> {
color: MyColors.primaryColor,
),
),
titleText: "My Subscriptions",
subTitle: "Silver",
titleText: LocaleKeys.mySubscription.tr(),
subTitle: LocaleKeys.silver.tr(),
isForLanguage: false,
needBorderBelow: true,
onTap: () {
@ -141,7 +141,7 @@ class _EditAccountPageState extends State<EditAccountPage> {
// color: MyColors.primaryColor,
),
),
titleText: "Subscriptions",
titleText: LocaleKeys.subscriptions.tr(),
subTitle: null,
isForLanguage: false,
needBorderBelow: false,
@ -152,7 +152,7 @@ class _EditAccountPageState extends State<EditAccountPage> {
],
).toWhiteContainer(width: double.infinity, pading: const EdgeInsets.all(12), borderRadius: 0),
10.height,
(AppState().currentAppType == AppType.provider) ? const Text("Provider") : const Text("Customer"),
(AppState().currentAppType == AppType.provider) ? const Text(LocaleKeys.provider) : const Text(LocaleKeys.customer),
],
),
),

@ -15,6 +15,8 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../classes/app_state.dart';
class ForgetPasswordPage extends StatefulWidget {
const ForgetPasswordPage({Key? key}) : super(key: key);
@ -133,11 +135,19 @@ class _ForgetPasswordPageState extends State<ForgetPasswordPage> {
}
Widget getCountry() {
bool isArabic = AppState().isArabic(context);
if (_country != null) {
List<DropValue> dropList = [];
_country!.data?.forEach(
(element) {
dropList.add(DropValue(element.id ?? 0, "${element.countryName ?? ""} ${element.countryCode ?? ""}", element.countryCode ?? ""));
// dropList.add(DropValue(element.id ?? 0, "${element.countryName ?? ""} ${element.countryCode ?? ""}", element.countryCode ?? ""));
dropList.add(
DropValue(
element.id ?? 0,
"${isArabic ? (element.countryNameN ?? element.countryName ?? "") : (element.countryName ?? "")} ${element.countryCode ?? ""}",
element.countryCode ?? "",
),
);
},
);
return Padding(

@ -107,7 +107,7 @@ class _LoginMethodSelectionPageState extends State<LoginMethodSelectionPage> {
],
),
150.height,
("Login To Another Account")
LocaleKeys.loginToAnotherAccount.tr()
.toText(
fontSize: 20,
letterSpacing: -1.44,

@ -179,15 +179,48 @@ class _LoginWithPasswordState extends State<LoginWithPassword> {
);
}
// Widget getCountry(BuildContext context) {
// if (_country != null) {
// List<DropValue> dropList = [];
// _country!.data?.forEach((element) {
// dropList.add(DropValue(element.id ?? 0, "${element.countryName ?? ""} ${element.countryCode ?? ""}", element.countryCode ?? ""));
// });
// return DropdownField(
// dropdownValue: tempCountry,
// (DropValue value) {
// countryCode = value.subValue;
// tempCountry = value;
// setState(() {});
// },
// list: dropList,
// hint: LocaleKeys.selectCountryCode.tr(),
// );
// } else {
// return const Center(
// child: CircularProgressIndicator(),
// );
// }
// }
Widget getCountry(BuildContext context) {
bool isArabic = AppState().isArabic(context);
if (_country != null) {
List<DropValue> dropList = [];
_country!.data?.forEach((element) {
dropList.add(DropValue(element.id ?? 0, "${element.countryName ?? ""} ${element.countryCode ?? ""}", element.countryCode ?? ""));
dropList.add(
DropValue(
element.id ?? 0,
"${isArabic ? (element.countryNameN ?? element.countryName ?? "") : (element.countryName ?? "")} ${element.countryCode ?? ""}",
element.countryCode ?? "",
),
);
});
return DropdownField(
dropdownValue: tempCountry,
(DropValue value) {
(DropValue value) {
countryCode = value.subValue;
tempCountry = value;
setState(() {});

@ -54,14 +54,15 @@ class _UpdateUserCityCountryState extends State<UpdateUserCityCountry> {
padding: const EdgeInsets.only(right: 20, left: 20, top: 12),
child: Builder(builder: (context) {
List<DropValue> userCountryDrop = [];
final isArabic = AppState().isArabic(context);
for (var element in uVM.userCountries!.data!) {
var countryid = country == null ? AppState().getUser.data!.userInfo!.countryId : country!.id;
print("Country ID" + countryid.toString());
if (countryid == element.id) {
print("Country Matched");
country = DropValue(element.id?.toInt() ?? 0, element.countryName ?? "", "");
country = DropValue(element.id?.toInt() ?? 0, isArabic ? element.countryNameN ?? "": element.countryName ?? "", "");
}
userCountryDrop.add(DropValue(element.id?.toInt() ?? 0, element.countryName ?? "", ""));
userCountryDrop.add(DropValue(element.id?.toInt() ?? 0, isArabic ? element.countryNameN ?? "": element.countryName ?? "", ""));
}
return DropdownField(
(DropValue value) async {
@ -82,13 +83,14 @@ class _UpdateUserCityCountryState extends State<UpdateUserCityCountry> {
padding: const EdgeInsets.only(right: 20, left: 20, top: 12),
child: Builder(builder: (context) {
List<DropValue> userCityDrop = [];
final isArabic = AppState().isArabic(context);
for (var element in uVM.userCities!.data!) {
var cid = city == null ? AppState().getUser.data!.userInfo!.cityId : city!.id;
print("City ID" + cid.toString());
if (cid == element.id) {
city = DropValue(element.id?.toInt() ?? 0, element.cityName ?? "", "");
city = DropValue(element.id?.toInt() ?? 0, isArabic ? element.cityNameN ?? "": element.cityName ?? "", "");
}
userCityDrop.add(DropValue(element.id?.toInt() ?? 0, element.cityName ?? "", ""));
userCityDrop.add(DropValue(element.id?.toInt() ?? 0, isArabic ? element.cityNameN ?? "": element.cityName ?? "", ""));
}
return DropdownField(
(DropValue value) {

@ -3,7 +3,6 @@ import 'dart:async';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:mc_common_app/classes/app_state.dart';
import 'package:mc_common_app/classes/consts.dart';
import 'package:mc_common_app/extensions/int_extensions.dart';
import 'package:mc_common_app/extensions/string_extensions.dart';
import 'package:mc_common_app/generated/locale_keys.g.dart';
@ -51,72 +50,74 @@ class _AddPhoneNumWidgetState extends State<AddPhoneNumWidget> {
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
child: Container(
width: double.infinity,
padding: const EdgeInsets.only(left: 16, right: 16, bottom: 16),
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
"Enter Phone Number".toText(fontSize: 16, isBold: true),
8.height,
Container(
decoration: Utils.containerColorRadiusBorderWidth(MyColors.white, 0, MyColors.darkPrimaryColor, 2),
margin: const EdgeInsets.all(0),
padding: const EdgeInsets.only(left: 8, right: 8),
width: null,
child: Row(
children: [
if (countryCode.isEmpty) ...[
LocaleKeys.selectCountryCode.tr().toText(color: borderColor, fontSize: 15, fontWeight: MyFonts.Medium).paddingOnly(top: 12, bottom: 12),
] else ...[
countryCode.toText(fontSize: 15, fontWeight: MyFonts.Medium).paddingOnly(top: 12, bottom: 12),
]
],
LocaleKeys.enterPhoneNumber.tr().toText(fontSize: 20, isBold: true, letterSpacing: -1.44),
30.height,
if (_country != null) ...[
DropdownField(
dropdownValue: countryCodeVal,
(DropValue value) {
AppState().setUserRegisterCountrySelection = value;
countryCode = value.subValue;
countryCodeVal = value;
setState(() {});
},
list: _country!.data?.map((element) {
return DropValue(
element.id ?? 0,
"${element.countryName ?? ""} ${element.countryCode ?? ""}",
element.countryCode ?? "",
);
}).toList() ?? [],
hint: LocaleKeys.selectCountryCode.tr(),
),
).onPress(() {
if (_country != null) {
getCountry(context);
}
}),
8.height,
TxtField(
keyboardType: TextInputType.phone,
hint: "Phone without country code ",
value: phoneNum,
isSidePaddingZero: true,
onChanged: (v) {
// Remove country code if user enters it
String cleanedPhone = v.trim();
// If country code is selected and phone starts with it, remove it
if (countryCode.isNotEmpty && cleanedPhone.startsWith(countryCode)) {
cleanedPhone = cleanedPhone.substring(countryCode.length).trim();
}
10.height,
TxtField(
keyboardType: TextInputType.phone,
hint: LocaleKeys.enterPhoneNumber.tr(),
value: phoneNum,
isSidePaddingZero: true,
onChanged: (v) {
// Remove country code if user enters it
String cleanedPhone = v.trim();
// If country code is selected and phone starts with it, remove it
if (countryCode.isNotEmpty && cleanedPhone.startsWith(countryCode)) {
cleanedPhone = cleanedPhone.substring(countryCode.length).trim();
}
// Also handle cases where user adds + before country code
if (countryCode.isNotEmpty && cleanedPhone.startsWith('+$countryCode')) {
cleanedPhone = cleanedPhone.substring(countryCode.length + 1).trim();
}
// Also handle cases where user adds + before country code
if (countryCode.isNotEmpty && cleanedPhone.startsWith('+$countryCode')) {
cleanedPhone = cleanedPhone.substring(countryCode.length + 1).trim();
}
// Handle case where user just types + followed by digits matching country code
if (countryCode.isNotEmpty && cleanedPhone.startsWith('+')) {
String codeWithoutPlus = countryCode.replaceAll('+', '');
if (cleanedPhone.substring(1).startsWith(codeWithoutPlus)) {
cleanedPhone = cleanedPhone.substring(codeWithoutPlus.length + 1).trim();
// Handle case where user just types + followed by digits matching country code
if (countryCode.isNotEmpty && cleanedPhone.startsWith('+')) {
String codeWithoutPlus = countryCode.replaceAll('+', '');
if (cleanedPhone.substring(1).startsWith(codeWithoutPlus)) {
cleanedPhone = cleanedPhone.substring(codeWithoutPlus.length + 1).trim();
}
}
}
setState(() {
phoneNum = cleanedPhone;
});
},
),
16.height,
setState(() {
phoneNum = cleanedPhone;
});
},
),
] else ...[
const Center(child: CircularProgressIndicator()),
],
30.height,
Row(
children: [
Expanded(
child: ShowFillButton(
title: "Cancel",
title: LocaleKeys.cancel.tr(),
backgroundColor: MyColors.greyButtonColor,
txtColor: Colors.black,
onPressed: () {
@ -147,22 +148,6 @@ class _AddPhoneNumWidgetState extends State<AddPhoneNumWidget> {
);
}
void getCountry(BuildContext context) {
List<DropValue> dropList = [];
_country!.data?.forEach((element) {
dropList.add(DropValue(element.id ?? 0, "${element.countryName ?? ""} ${element.countryCode ?? ""}", element.countryCode ?? ""));
});
return showCustomBottomPicker(
items: dropList,
onItemSelected: (value) {
AppState().setUserRegisterCountrySelection = value;
countryCode = value.subValue;
countryCodeVal = value;
setState(() {});
},
context: context);
}
bool validation() {
bool isValid = true;
if (countryCode.isEmpty) {
@ -177,73 +162,3 @@ class _AddPhoneNumWidgetState extends State<AddPhoneNumWidget> {
return isValid;
}
}
class CustomBottomPicker extends StatelessWidget {
final List<DropValue> items;
final void Function(DropValue selectedItem) onItemSelected;
const CustomBottomPicker({
super.key,
required this.items,
required this.onItemSelected,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => Navigator.of(context).pop(),
child: Container(
color: Colors.black.withOpacity(0.4),
child: DraggableScrollableSheet(
initialChildSize: 0.2,
maxChildSize: 0.4,
minChildSize: 0.2,
expand: true,
builder: (context, scrollController) {
return Container(
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
child: ListView.builder(
controller: scrollController,
itemCount: items.length,
shrinkWrap: true,
physics: const AlwaysScrollableScrollPhysics(),
itemBuilder: (context, index) {
return ListTile(
title: Text(items[index].value),
onTap: () {
onItemSelected(items[index]);
Navigator.of(context).pop();
},
);
},
),
);
},
),
),
);
}
}
// Function to show the bottom picker
void showCustomBottomPicker({
required BuildContext context,
required List<DropValue> items,
required void Function(DropValue selectedItem) onItemSelected,
}) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
isDismissible: false,
backgroundColor: Colors.transparent,
builder: (context) {
return CustomBottomPicker(
items: items,
onItemSelected: onItemSelected,
);
},
);
}

@ -95,24 +95,47 @@ class BranchDetailCard extends StatelessWidget {
],
),
8.height,
// if (services != null)
// Column(
// children: services!
// .take(2)
// .map(
// (e) => Row(
// children: [
// //TODO: Needs to add icon in the future when added from the provider
// // MyAssets.maintenanceIcon.buildSvg(),
// // 8.width,
// e.serviceDescription.toString().toText(fontSize: 12)
// ],
// ),
// )
// .toList(),
// ),
if (services != null)
Column(
children: services!
.take(2)
.map(
(e) => Row(
children: [
//TODO: Needs to add icon in the future when added from the provider
// MyAssets.maintenanceIcon.buildSvg(),
// 8.width,
e.serviceDescription.toString().toText(fontSize: 12)
],
),
)
(e) {
bool isArabic = AppState().isArabic(context);
String serviceText = isArabic
? e.serviceDescriptionN.toString()
: e.serviceDescription.toString();
return Row(
children: [
//TODO: Needs to add icon in the future when added from the provider
// MyAssets.maintenanceIcon.buildSvg(),
// 8.width,
serviceText.toText(fontSize: 12)
],
);
},
)
.toList(),
),
if (services != null && services!.length > 2)
("+${services!.length - 2} more").toText(
("+${services!.length - 2} ${LocaleKeys.more.tr()}").toText(
color: MyColors.primaryColor,
isUnderLine: true,
isBold: true,

@ -1,11 +1,14 @@
import 'dart:developer';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:mc_common_app/extensions/string_extensions.dart';
import 'package:mc_common_app/models/general_models/widgets_models.dart';
import 'package:mc_common_app/theme/colors.dart';
import 'package:mc_common_app/widgets/extensions/extensions_widget.dart';
import '../../classes/app_state.dart';
class FiltersList extends StatelessWidget {
final List<FilterListModel> filterList;
final Function(int, int) onFilterTapped;
@ -24,6 +27,7 @@ class FiltersList extends StatelessWidget {
@override
Widget build(BuildContext context) {
final isArabic = AppState().isArabic(context);
return SizedBox(
height: 37,
width: double.infinity,
@ -32,6 +36,8 @@ class FiltersList extends StatelessWidget {
itemCount: filterList.length,
scrollDirection: Axis.horizontal,
itemBuilder: (BuildContext context, int index) {
print("==================================================");
print(filterList[index].title);
return InkWell(
onTap: () {
onFilterTapped(index, filterList[index].id);
@ -62,11 +68,22 @@ class FiltersList extends StatelessWidget {
],
],
// Text displayed in the chip
filterList[index].title.toText(
fontSize: 13,
letterSpacing: -0.14,
color: filterList[index].isSelected ? MyColors.white : null,
),
(isArabic
? (filterList[index].titleN.isNotEmpty ? filterList[index].titleN : filterList[index].title) : filterList[index].title).toText(
fontSize: 13,
letterSpacing: -0.14,
color: filterList[index].isSelected ? MyColors.white : null,
)
// (isArabic
// ? (filterList[index].titleN ?? filterList[index].title)
// : filterList[index].title)
// .tr()
// .toText(
// // filterList[index].title.tr().toText(
// fontSize: 13,
// letterSpacing: -0.14,
// color: filterList[index].isSelected ? MyColors.white : null,
// ),
],
),
),

@ -46,44 +46,44 @@ class ProviderDetailCard extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
title.toText(fontSize: 16, isBold: true),
Row(
children: [
("${LocaleKeys.totalBranches.tr()}:").toText(color: MyColors.lightTextColor, fontSize: 12),
4.width,
totalBranches.toText(fontSize: 12, isBold: true),
],
),
if (description.isNotEmpty) ...[
Row(
children: [
("${LocaleKeys.description.tr()}:").toText(color: MyColors.lightTextColor, fontSize: 12),
4.width,
description.toText(fontSize: 12, isBold: true),
],
),
],
Row(
children: [
(isFromFavoriteList ? LocaleKeys.addToFavoritesOn.tr() : LocaleKeys.memberSince.tr()).toText(color: MyColors.lightTextColor, fontSize: 12),
4.width,
startedSince.toText(fontSize: 12, isBold: true),
],
),
],
),
),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
title.toText(fontSize: 16, isBold: true),
Row(
children: [
("${LocaleKeys.totalBranches.tr()}:").toText(color: MyColors.lightTextColor, fontSize: 12),
4.width,
totalBranches.toText(fontSize: 12, isBold: true),
],
),
if (description.isNotEmpty) ...[
Row(
children: [
("${LocaleKeys.description.tr()}:").toText(color: MyColors.lightTextColor, fontSize: 12),
4.width,
description.toText(fontSize: 12, isBold: true),
],
),
],
Row(
children: [
(isFromFavoriteList ? LocaleKeys.addToFavoritesOn.tr() : LocaleKeys.memberSince.tr()).toText(color: MyColors.lightTextColor, fontSize: 12),
4.width,
startedSince.toText(fontSize: 12, isBold: true),
],
),
],
),
),
],
),
],
),
),

@ -75,12 +75,7 @@ class _DropdownFieldState extends State<DropdownField> {
// Show disabled text field when not selectable
if (!widget.isSelectAble)
Container(
decoration: Utils.containerColorRadiusBorderWidth(
MyColors.greyShadowColor,
0,
MyColors.greyACColor,
2
),
decoration: Utils.containerColorRadiusBorderWidth(MyColors.greyShadowColor, 0, MyColors.greyACColor, 2),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14),
width: double.infinity,
child: (dropdownValue?.value ?? "").toText(
@ -90,16 +85,9 @@ class _DropdownFieldState extends State<DropdownField> {
),
)
else
// Show normal dropdown when selectable
// Show normal dropdown when selectable
Container(
decoration: widget.showAppointmentPickerVariant
? null
: Utils.containerColorRadiusBorderWidth(
MyColors.white,
0,
MyColors.darkPrimaryColor,
2
),
decoration: widget.showAppointmentPickerVariant ? null : Utils.containerColorRadiusBorderWidth(MyColors.white, 0, MyColors.darkPrimaryColor, 2),
margin: const EdgeInsets.all(0),
padding: const EdgeInsets.only(left: 8, right: 8),
width: widget.showAppointmentPickerVariant ? 170 : null,
@ -127,15 +115,11 @@ class _DropdownFieldState extends State<DropdownField> {
},
onTap: widget.onTap,
items: (widget.list ?? defaultV).map<DropdownMenuItem<DropValue>>(
(DropValue value) {
(DropValue value) {
return DropdownMenuItem<DropValue>(
value: value,
enabled: value.isEnabled ?? true,
child: value.value.toText(
fontSize: 15,
color: value.isEnabled == false ? MyColors.darkTextColor : null,
fontWeight: MyFonts.Medium
),
child: value.value.toText(fontSize: 15, color: value.isEnabled == false ? MyColors.darkTextColor : null, fontWeight: MyFonts.Medium),
);
},
).toList(),

@ -371,7 +371,10 @@ extension BuildSVG on String? {
return SizedBox(
height: height,
width: width,
child: Image.asset(MyAssets.brokenImage).paddingAll(10),
child: Image.asset(
MyAssets.brokenImage,
fit: BoxFit.fill,
),
);
}
@ -382,7 +385,11 @@ extension BuildSVG on String? {
height: height,
width: width,
decoration: BoxDecoration(border: Border.all(color: MyColors.roundedCrossBgColor)),
child: const Icon(Icons.image_not_supported_outlined, color: MyColors.roundedCrossBgColor),
child: Icon(
Icons.image_not_supported_outlined,
color: MyColors.roundedCrossBgColor,
size: width,
),
);
},
loadingBuilder: (BuildContext context, Widget? child, ImageChunkEvent? imageChunk) {

@ -0,0 +1,228 @@
import 'package:flutter/material.dart';
import 'package:shimmer/shimmer.dart';
/// Shimmer for horizontal appointment slider
class AppointmentSliderShimmer extends StatelessWidget {
const AppointmentSliderShimmer({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return SizedBox(
height: 180,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: 3,
padding: const EdgeInsets.symmetric(horizontal: 21),
itemBuilder: (context, index) {
return Shimmer.fromColors(
baseColor: Colors.grey[300]!,
highlightColor: Colors.grey[100]!,
child: Container(
width: MediaQuery.of(context).size.width * 0.85,
margin: EdgeInsets.only(right: index < 2 ? 12 : 0),
decoration: const BoxDecoration(
color: Colors.white,
),
),
);
},
),
);
}
}
/// Shimmer for appointments list (vertical list of appointment cards)
class AppointmentsListShimmer extends StatelessWidget {
final int itemCount;
const AppointmentsListShimmer({super.key, this.itemCount = 5});
@override
Widget build(BuildContext context) {
return Shimmer.fromColors(
baseColor: Colors.grey[300]!,
highlightColor: Colors.grey[100]!,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 21),
child: ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: itemCount,
separatorBuilder: (context, index) => const SizedBox(height: 12),
itemBuilder: (context, index) {
return Container(
height: 140,
decoration: const BoxDecoration(
color: Colors.white,
),
);
},
),
),
);
}
}
/// Shimmer for branches list (vertical list of branch cards)
class BranchesListShimmer extends StatelessWidget {
final int itemCount;
const BranchesListShimmer({Key? key, this.itemCount = 5}) : super(key: key);
@override
Widget build(BuildContext context) {
return Shimmer.fromColors(
baseColor: Colors.grey[300]!,
highlightColor: Colors.grey[100]!,
child: Padding(
padding: const EdgeInsets.all(12),
child: ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: itemCount,
separatorBuilder: (context, index) => const SizedBox(height: 12),
itemBuilder: (context, index) {
return Container(
height: 160,
decoration: const BoxDecoration(
color: Colors.white,
),
);
},
),
),
);
}
}
/// Shimmer for requests list (vertical list of request cards)
class RequestsListShimmer extends StatelessWidget {
final int itemCount;
const RequestsListShimmer({Key? key, this.itemCount = 5}) : super(key: key);
@override
Widget build(BuildContext context) {
return Shimmer.fromColors(
baseColor: Colors.grey[300]!,
highlightColor: Colors.grey[100]!,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 21),
child: ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: itemCount,
separatorBuilder: (context, index) => const SizedBox(height: 12),
itemBuilder: (context, index) {
return Container(
height: 120,
decoration: const BoxDecoration(
color: Colors.white,
),
);
},
),
),
);
}
}
/// Shimmer for services grid (3 columns)
class ServicesGridShimmer extends StatelessWidget {
const ServicesGridShimmer({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Shimmer.fromColors(
baseColor: Colors.grey[300]!,
highlightColor: Colors.grey[100]!,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 21),
child: GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
mainAxisSpacing: 12,
crossAxisSpacing: 12,
childAspectRatio: 0.9,
),
itemCount: 6,
itemBuilder: (context, index) {
return Container(
decoration: const BoxDecoration(
color: Colors.white,
),
);
},
),
),
);
}
}
/// Shimmer for ads list
class AdsListShimmer extends StatelessWidget {
final int itemCount;
const AdsListShimmer({Key? key, this.itemCount = 3}) : super(key: key);
@override
Widget build(BuildContext context) {
return Shimmer.fromColors(
baseColor: Colors.grey[300]!,
highlightColor: Colors.grey[100]!,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 21),
child: ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: itemCount,
separatorBuilder: (context, index) => const SizedBox(height: 12),
itemBuilder: (context, index) {
return Container(
height: 140,
decoration: const BoxDecoration(
color: Colors.white,
),
);
},
),
),
);
}
}
/// Shimmer for provider branches grid
class BranchesGridShimmer extends StatelessWidget {
const BranchesGridShimmer({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Shimmer.fromColors(
baseColor: Colors.grey[300]!,
highlightColor: Colors.grey[100]!,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 21),
child: GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 12,
crossAxisSpacing: 12,
childAspectRatio: 0.75,
),
itemCount: 4,
itemBuilder: (context, index) {
return Container(
decoration: const BoxDecoration(
color: Colors.white,
),
);
},
),
),
);
}
}

File diff suppressed because it is too large Load Diff

@ -43,6 +43,7 @@ dependencies:
change_app_package_name: ^1.3.0
share_plus: any
toastification: ^2.3.0
geolocator_android: 4.5.5
# Firebase Packages

Loading…
Cancel
Save