Merge branch 'faiz_development_common' into fatima_dev

# Conflicts:
#	lib/views/advertisement/components/ads_list_widget.dart
pull/7/head
Rand Alshammari 3 months ago
commit 1bbb706d8c

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

@ -254,6 +254,9 @@
"sendOTP": "إرسال رمز OTP",
"noUserAddedYet": "لم يتم إضافة مستخدم بعد",
"assignDealerUser": "تعيين مستخدم تاجر",
"confirmAssignDealerUser": "هل أنت متأكد من رغبتك في تعيين مستخدم التاجر هذا للفرع؟",
"removeDealerUser": "إزالة مستخدم تاجر",
"confirmRemoveDealerUser": "هل أنت متأكد من رغبتك في إزالة مستخدم التاجر هذا من الفرع؟",
"createNewUser": "إنشاء مستخدم جديد",
"dealerUser": "مستخدم تاجر",
"setorEditBranchSchedule": "تعيين أو تحرير جدول الفروع",
@ -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": "اقرأ المزيد من التقييمات",
@ -885,6 +895,12 @@
"month": "شهر",
"currentPackage": "الحزمة الحالية",
"forVar": "لمدة",
"branch": "الفرع"
"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",
@ -883,5 +893,11 @@
"month": "Month",
"currentPackage": "Current Package",
"forVar": "for",
"branch": "branch"
"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) {

@ -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";

@ -290,6 +290,8 @@ extension AppointmentStatusToInt on AppointmentStatusEnum {
case AppointmentStatusEnum.upcoming:
return 11;
case AppointmentStatusEnum.served:
return 12;
case AppointmentStatusEnum.allAppointments:
return 0;
@ -386,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;
}
@ -440,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;
}
@ -454,6 +460,9 @@ extension RequestTypeStatusToInt on RequestsTypeEnum {
case RequestsTypeEnum.serviceRequest:
return 2;
case RequestsTypeEnum.other:
return 3;
default:
return 0;
}

@ -270,6 +270,9 @@ class CodegenLoader extends AssetLoader{
"sendOTP": "إرسال رمز OTP",
"noUserAddedYet": "لم يتم إضافة مستخدم بعد",
"assignDealerUser": "تعيين مستخدم تاجر",
"confirmAssignDealerUser": "هل أنت متأكد من رغبتك في تعيين مستخدم التاجر هذا للفرع؟",
"removeDealerUser": "إزالة مستخدم تاجر",
"confirmRemoveDealerUser": "هل أنت متأكد من رغبتك في إزالة مستخدم التاجر هذا من الفرع؟",
"createNewUser": "إنشاء مستخدم جديد",
"dealerUser": "مستخدم تاجر",
"setorEditBranchSchedule": "تعيين أو تحرير جدول الفروع",
@ -433,6 +436,9 @@ class CodegenLoader extends AssetLoader{
"accidentFree": "خالية من الحوادث",
"brandNew": "جديدة",
"used": "مستعملة",
"all": "الكل",
"newSparePart": "قطع غيار جديدة",
"usedSparePart": "قطع غيار مستعملة",
"hasYourCarHadAccident": "هل تعرضت سيارتك لأي حادث؟",
"odometerReading": "قراءة عداد المسافات (كم)",
"currentlyInUse": "قيد الاستخدام حاليًا",
@ -606,6 +612,9 @@ class CodegenLoader extends AssetLoader{
"myNearbyBranches": "فروعي القريبة",
"myRecentBranches": "فروعي الأخيرة",
"favoriteList": "قائمة المفضلة",
"myFavouriteProviders": "مزودي الخدمة المفضلين",
"myFavouriteAds": "إعلاناتي المفضلة",
"noFavouriteAds": "لا توجد إعلانات مفضلة",
"noReviewsBranch": "هذا الفرع لا يوجد لديه تعليقات.",
"readLess": "اقرأ أقل",
"readMoreReviews": "اقرأ المزيد من التقييمات",
@ -833,7 +842,7 @@ class CodegenLoader extends AssetLoader{
"specialRequestsChats": "دردشات الطلبات الخاصة",
"selectDeliveryOption": "اختر خيار التوصيل",
"deliveryOptions": "خيارات التوصيل",
"selfPickup": "الاستلام الذاتي",
"selfPickup": "استلام ذاتي",
"updateYourLocationInfo": "قم بتحديث معلومات موقعك",
"initiateSelfPickup": "بدء الاستلام الذاتي",
"selfPickupStatus": "حالة الاستلام الذاتي",
@ -841,43 +850,43 @@ class CodegenLoader extends AssetLoader{
"scheduleDeletedSuccessfully": "تم حذف الجدول بنجاح",
"addValidAddress": "رجى إضافة عنوان صالح",
"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": "جميع الاعلانات",
"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": "قيد الدفع",
@ -901,7 +910,13 @@ class CodegenLoader extends AssetLoader{
"month": "شهر",
"currentPackage": "الحزمة الحالية",
"forVar": "لمدة",
"branch": "الفرع"
"branch": "الفرع",
"shareAd": "مشاركة الإعلان",
"checkOutThisAd": "تفقد هذه السيارة الرائعة!",
"downloadApp": "تحميل تطبيق موّاتر:",
"androidApp": "أندرويد",
"iosApp": "آيفون",
"pleaseWait": "يرجى الانتظار..."
};
static const Map<String,dynamic> _en_US = {
"firstTimeLogIn": "First Time Log In",
@ -1159,6 +1174,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",
@ -1323,6 +1341,9 @@ 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)",
"currentlyInUse": "Currently in Use",
@ -1496,6 +1517,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",
@ -1764,12 +1788,12 @@ static const Map<String,dynamic> _en_US = {
"km": "km",
"items": "items",
"no_items": "No items",
"All Ads":"All Ads",
"reserveCancel": " Reserve Cancel",
"allAds": "All Ads",
"reserveCancel": "Reserve Cancel",
"pendingForReview": "Pending For Review",
"pendingForPayment": "Pending For Payment",
"rejected_n": "Rejected",
"canceled": "Canceled",
"canceled": "Cancelled",
"pendingForPost": "Pending For Post",
"active_n": "Active",
"expired_n": "Expired",
@ -1785,11 +1809,16 @@ static const Map<String,dynamic> _en_US = {
"pleaseAddValidItemDescription": "Please add valid item description",
"pleaseAddValidItemPrice": "Please add valid item price",
"remaining": "remaining",
"month": "Month",
"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,6 +401,9 @@ 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 currentlyInUse = 'currentlyInUse';
@ -571,6 +577,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';
@ -837,6 +846,7 @@ abstract class LocaleKeys {
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';
@ -846,7 +856,7 @@ abstract class LocaleKeys {
static const pendingForReview = 'pendingForReview';
static const pendingForPayment = 'pendingForPayment';
static const rejected_n = 'rejected_n';
static const cancelled_n = 'cancelled_n';
static const canceled = 'canceled';
static const pendingForPost = 'pendingForPost';
static const active_n = 'active_n';
static const expired_n = 'expired_n';
@ -866,20 +876,11 @@ abstract class LocaleKeys {
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';
}

@ -46,6 +46,7 @@ class AdDetailsModel {
AdReserveStatus? adReserveStatus;
bool? isMyAd;
bool? isReservedByMe;
bool? isAdFavorite;
String? phoneNo;
String? whatsAppNo;
String? adOwnerName;
@ -95,6 +96,7 @@ class AdDetailsModel {
this.adReserveStatus,
this.isMyAd,
this.isReservedByMe,
this.isAdFavorite,
this.phoneNo,
this.whatsAppNo,
this.adOwnerName,
@ -164,6 +166,7 @@ class AdDetailsModel {
createdByRoleEnum = (json['createdByRole'] as int).toCreatedByRoleEnum();
isMyAd = isMyAds;
isReservedByMe = false;
isAdFavorite = json['isFavourite'] ?? false;
totalItemsCount = totalItems;
}
}

@ -21,7 +21,7 @@ class EnumsModel {
id: json["id"],
enumTypeId: json["enumTypeID"],
enumValueStr: json["enumValueStr"],
enumValueStrN: json["enumValueStrN"],
enumValueStrN: json["enumValueStrN"] ?? json["enumValueStr"],
enumValueStrDes: json["enumValueStrDes"] ?? json["enumValueStr"],
enumValue: json["enumValue"],
isActive: json["isActive"],

@ -1,12 +1,14 @@
class FilterListModel {
String title;
String? titleN;
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, this.titleN});
FilterListModel({required this.id, required this.isSelected, this.isCustom = false, this.iconUrl = "", required this.title, this.titleN = "", this.description = "", this.descriptionN = "",});
}
class SelectionModel {

@ -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 = {
@ -560,8 +596,7 @@ class AdsRepoImp implements AdsRepo {
String token = appState.getUser.data!.accessToken ?? "";
GenericRespModel adsGenericModel = await apiClient.postJsonForObject(
(json) => GenericRespModel.fromJson(json),
ApiConsts.
adsExtendDurationCreate,
ApiConsts.adsExtendDurationCreate,
postParams,
token: token,
);

@ -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);

@ -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,
);
@ -440,27 +444,27 @@ class Utils {
case BranchStatusEnum.approvedOrActive:
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 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({
required String text,

@ -89,6 +89,7 @@ class AdVM extends BaseVM {
AdDetailsModel? previousAdDetails;
List<AdDetailsModel> myDraftAds = [];
List<AdDetailsModel> favouriteAds = [];
List<AdDetailsModel> exploreAds = [];
List<AdDetailsModel> exploreAdsFilteredList = [];
List<AdDetailsModel> myAdsFilteredList = [];
@ -338,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;
@ -365,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;
@ -740,7 +772,7 @@ class AdVM extends BaseVM {
notifyListeners();
}
int isInUsed = 0;
int isInUsed = 0; // 0 = No, 1 = Yes
void updateIsInUsed(int value) {
isInUsed = value;

@ -52,8 +52,7 @@ class AppointmentsVM extends BaseVM {
final BranchRepo branchRepo;
final AppointmentRepo appointmentRepo;
AppointmentsVM(
{required this.commonServices, required this.appointmentRepo, required this.commonRepo, required this.branchRepo});
AppointmentsVM({required this.commonServices, required this.appointmentRepo, required this.commonRepo, required this.branchRepo});
bool isUpcomingEnabled = true;
bool isFetchingLists = false;
@ -80,8 +79,7 @@ class AppointmentsVM extends BaseVM {
List<ItemData> allSelectedItemsInAppointments = [];
bool ifItemAlreadySelected(int id) {
int indexFound = allSelectedItemsInAppointments.indexWhere((
element) => element.id == id);
int indexFound = allSelectedItemsInAppointments.indexWhere((element) => element.id == id);
log("already selected at: $indexFound");
if (indexFound != -1) {
return true;
@ -109,8 +107,7 @@ class AppointmentsVM extends BaseVM {
}
}
Future<void> onPayNowPressedForAppointment(
{required BuildContext context, required int appointmentID}) async {
Future<void> onPayNowPressedForAppointment({required BuildContext context, required int appointmentID}) async {
context.read<PaymentVM>().updateAppointmentIdsForPayment(
ids: [appointmentID]);
navigateWithName(context, AppRoutes.paymentMethodsView,
@ -168,8 +165,7 @@ class AppointmentsVM extends BaseVM {
}
}
Future<void> onConfirmAppointmentPressed(
{required BuildContext context, required appointmentId}) async {
Future<void> onConfirmAppointmentPressed({required BuildContext context, required appointmentId}) async {
context.read<PaymentVM>().updateAppointmentIdsForPayment(
ids: [appointmentId]);
navigateWithName(context, AppRoutes.paymentMethodsView,
@ -224,8 +220,7 @@ class AppointmentsVM extends BaseVM {
}
}
Future<void> onCancelAppointmentPressed(
{required BuildContext context, required AppointmentListModel appointmentListModel}) async {
Future<void> onCancelAppointmentPressed({required BuildContext context, required AppointmentListModel appointmentListModel}) async {
Utils.showLoading(context);
try {
GenericRespModel genericRespModel = await appointmentRepo
@ -378,20 +373,19 @@ class AppointmentsVM extends BaseVM {
for (int i = 0; i < myAppointmentsEnum.length; i++) {
appointmentsFilterOptions.add(FilterListModel(
title: myAppointmentsEnum[i].enumValueStr,
titleN: myAppointmentsEnum[i].enumValueStrN,
titleN: myAppointmentsEnum[i].enumValueStrN,
isSelected: false,
id: myAppointmentsEnum[i].enumValue));
}
appointmentsFilterOptions.insert(
0, FilterListModel(title: "All Appointments",titleN: "جميع الفروع", isSelected: true, id: 0));
0, FilterListModel(title: "All Appointments", titleN: "جميع الفروع", isSelected: true, id: 0));
notifyListeners();
}
bool isAppointmentLoading = false;
applyFilterOnAppointmentsVMForProviders(
{int? branchID, required AppointmentStatusEnum appointmentStatusEnum, bool isNeedCustomerFilter = false, bool shouldPopulateUpcoming = false}) async {
applyFilterOnAppointmentsVMForProviders({int? branchID, required AppointmentStatusEnum appointmentStatusEnum, bool isNeedCustomerFilter = false, bool shouldPopulateUpcoming = false}) async {
if (appointmentsFilterOptions.isEmpty) return;
for (var value in appointmentsFilterOptions) {
value.isSelected = false;
@ -430,8 +424,7 @@ class AppointmentsVM extends BaseVM {
notifyListeners();
}
applyFilterOnAppointmentsVMForCustomers(
{required AppointmentStatusEnum appointmentStatusEnum, bool shouldPopulateUpcoming = false}) async {
applyFilterOnAppointmentsVMForCustomers({required AppointmentStatusEnum appointmentStatusEnum, bool shouldPopulateUpcoming = false}) async {
log("appointmentsFilterOptions: ${appointmentsFilterOptions.isEmpty}");
if (appointmentsFilterOptions.isEmpty) return;
for (var value in appointmentsFilterOptions) {
@ -460,8 +453,7 @@ class AppointmentsVM extends BaseVM {
notifyListeners();
}
List<AppointmentListModel> findAppointmentsBasedOnCustomers(
List<AppointmentListModel> appointments) {
List<AppointmentListModel> findAppointmentsBasedOnCustomers(List<AppointmentListModel> appointments) {
Set<int> uniqueCustomerIDs = <int>{};
for (var item in appointments) {
uniqueCustomerIDs.add(item.customerID ?? 0);
@ -511,8 +503,7 @@ class AppointmentsVM extends BaseVM {
}
}
Future<bool> updateAppointmentStatus(
{required int appointmentId, required AppointmentStatusEnum appointmentStatusEnum, bool isNeedToRebuild = false}) async {
Future<bool> updateAppointmentStatus({required int appointmentId, required AppointmentStatusEnum appointmentStatusEnum, bool isNeedToRebuild = false}) async {
if (isNeedToRebuild) setState(ViewState.busy);
try {
GenericRespModel genericRespModel = await appointmentRepo
@ -599,8 +590,7 @@ class AppointmentsVM extends BaseVM {
.length;
}
updateSelectedAppointmentDate(
{required int dateIndex, required int scheduleIndex}) {
updateSelectedAppointmentDate({required int dateIndex, required int scheduleIndex}) {
for (var element in serviceAppointmentScheduleList[scheduleIndex]
.customTimeDateSlotList!) {
element.date!.isSelected = false;
@ -622,8 +612,7 @@ class AppointmentsVM extends BaseVM {
notifyListeners();
}
updateSelectedAppointmentSlotByDate(
{required int scheduleIndex, required int slotIndex}) {
updateSelectedAppointmentSlotByDate({required int scheduleIndex, required int slotIndex}) {
for (var element in serviceAppointmentScheduleList[scheduleIndex]
.customTimeDateSlotList!) {
for (var element in element.availableSlots!) {
@ -652,8 +641,7 @@ class AppointmentsVM extends BaseVM {
int selectedSubServicesCounter = 0;
void onItemUpdateOrSelected(int index, bool selected, int itemId) {
int serviceIndex = servicesInCurrentAppointment.indexWhere((
element) => element.serviceId == currentServiceSelection!.serviceId!);
int serviceIndex = servicesInCurrentAppointment.indexWhere((element) => element.serviceId == currentServiceSelection!.serviceId!);
serviceItemsFromApi[index].isUpdateOrSelected = selected;
serviceItemsFromApi[index].isHomeSelected = isHomeTapped;
@ -983,8 +971,7 @@ class AppointmentsVM extends BaseVM {
notifyListeners();
}
Future<void> onRescheduleAppointmentPressed(
{required BuildContext context, required AppointmentListModel appointmentListModel}) async {
Future<void> onRescheduleAppointmentPressed({required BuildContext context, required AppointmentListModel appointmentListModel}) async {
Utils.showLoading(context);
List<String> serviceItemIdsForHome = [];
@ -1024,8 +1011,7 @@ class AppointmentsVM extends BaseVM {
notifyListeners();
}
Future<void> onRescheduleAppointmentConfirmPressed(
{required BuildContext context, required int appointmentId, required int selectedSlotId}) async {
Future<void> onRescheduleAppointmentConfirmPressed({required BuildContext context, required int appointmentId, required int selectedSlotId}) async {
Utils.showLoading(context);
try {
GenericRespModel genericRespModel = await appointmentRepo
@ -1066,8 +1052,7 @@ class AppointmentsVM extends BaseVM {
}
}
Future buildComplaintBottomSheet(
{required int customerID, required int complainType, required String customerName, required BuildContext context}) {
Future buildComplaintBottomSheet({required int customerID, required int complainType, required String customerName, required BuildContext context}) {
return showModalBottomSheet(
context: context,
isScrollControlled: true,
@ -1129,8 +1114,7 @@ class AppointmentsVM extends BaseVM {
);
}
Future<bool> reportComplaintAgainstCustomer(
{required int customerID, required String comment, required int complaintType, required BuildContext context}) async {
Future<bool> reportComplaintAgainstCustomer({required int customerID, required String comment, required int complaintType, required BuildContext context}) async {
Utils.showLoading(context);
try {
GenericRespModel genericRespModel = await commonRepo
@ -1148,8 +1132,7 @@ class AppointmentsVM extends BaseVM {
}
}
Future<ProviderContactInfoModel?> getProvidersContactInfo(
{required int providerId, required BuildContext context}) async {
Future<ProviderContactInfoModel?> getProvidersContactInfo({required int providerId, required BuildContext context}) async {
Utils.showLoading(context);
try {
ProviderContactInfoModel contactInfoModel = await commonRepo
@ -1179,7 +1162,8 @@ class AppointmentsVM extends BaseVM {
Services services = await branchRepo.fetchServicesByCategoryId(
serviceCategoryId: -1);
services.data?.forEach((element) {
branchesCategoriesFilterOptions.add(FilterListModel(id: element.id ?? 0,
branchesCategoriesFilterOptions.add(FilterListModel(
id: element.id ?? 0,
isSelected: false,
iconUrl: element.serviceIconUrl ?? "",
title: element.description ?? "",
@ -1191,14 +1175,12 @@ class AppointmentsVM extends BaseVM {
setState(ViewState.idle);
}
Future<void> getAllNearBranches(
{bool isNeedToRebuild = false, bool isFromRefresh = false}) async {
Future<void> getAllNearBranches({bool isNeedToRebuild = false, bool isFromRefresh = false}) async {
nearbyBranches.clear();
if (isNeedToRebuild) setState(ViewState.busy);
if (isFromRefresh) {
var selectedFilter = branchesCategoriesFilterOptions.firstWhere((
element) => element.isSelected);
var selectedFilter = branchesCategoriesFilterOptions.firstWhere((element) => element.isSelected);
log("selectedFilter: ${selectedFilter.id}");
if (selectedFilter.id == 0) {
@ -1305,8 +1287,7 @@ class AppointmentsVM extends BaseVM {
// Provider Filter
List<DropValue> branchFilterProviderSearchHistory = [];
void removeBranchFilterProviderSearchHistory(
{bool isClear = false, required int index}) {
void removeBranchFilterProviderSearchHistory({bool isClear = false, required int index}) {
if (isClear) {
branchFilterProviderSearchHistory.clear();
notifyListeners();
@ -1349,8 +1330,7 @@ class AppointmentsVM extends BaseVM {
// Category Filter
List<DropValue> branchFilterCategorySearchHistory = [];
void removeBranchFilterCategorySearchHistory(
{bool isClear = false, required int index}) {
void removeBranchFilterCategorySearchHistory({bool isClear = false, required int index}) {
if (isClear) {
branchFilterCategorySearchHistory.clear();
notifyListeners();
@ -1393,8 +1373,7 @@ class AppointmentsVM extends BaseVM {
// Services Filter
List<DropValue> branchFilterServicesSearchHistory = [];
void removeBranchFilterServicesSearchHistory(
{bool isClear = false, required int index}) {
void removeBranchFilterServicesSearchHistory({bool isClear = false, required int index}) {
if (isClear) {
branchFilterServicesSearchHistory.clear();
notifyListeners();
@ -1448,8 +1427,7 @@ class AppointmentsVM extends BaseVM {
return false;
}
ifAlreadyExistForStringValue(
{required List<String> list, required String value}) {
ifAlreadyExistForStringValue({required List<String> list, required String value}) {
int index = list.indexWhere((element) {
return element == value;
});
@ -1627,8 +1605,7 @@ class AppointmentsVM extends BaseVM {
setState(ViewState.idle);
}
Future<void> getBranchesBasedOnCategoryFilters(
{required int serviceId}) async {
Future<void> getBranchesBasedOnCategoryFilters({required int serviceId}) async {
setState(ViewState.busy);
if (serviceId == 0) {
await getAllNearBranches();
@ -1647,8 +1624,7 @@ class AppointmentsVM extends BaseVM {
List<DropValue> branchesDropList = [];
Future<void> fetchAllBranchesBySelectedProviderId(
{required List<int> providersIdsList}) async {
Future<void> fetchAllBranchesBySelectedProviderId({required List<int> providersIdsList}) async {
branchesDropList.clear();
setOnlyState(ViewState.busy);
List<BranchDetailModel> providers = await branchRepo.getBranchesByFilters(
@ -1682,8 +1658,7 @@ class AppointmentsVM extends BaseVM {
// Provider Filter For Appointments
List<DropValue> appointmentFilterProviderSearchHistory = [];
void removeAppointmentFilterProviderSearchHistory(
{bool isClear = false, required int index}) {
void removeAppointmentFilterProviderSearchHistory({bool isClear = false, required int index}) {
if (isClear) {
appointmentFilterProviderSearchHistory.clear();
notifyListeners();
@ -1740,8 +1715,7 @@ class AppointmentsVM extends BaseVM {
List<DropValue> appointmentFilterBranchSearchHistory = [];
void removeAppointmentFilterBranchSearchHistory(
{bool isClear = false, required int index}) {
void removeAppointmentFilterBranchSearchHistory({bool isClear = false, required int index}) {
if (isClear) {
appointmentFilterBranchSearchHistory.clear();
notifyListeners();
@ -1784,8 +1758,7 @@ class AppointmentsVM extends BaseVM {
List<DropValue> appointmentFilterCategorySearchHistory = [];
void removeAppointmentFilterCategorySearchHistory(
{bool isClear = false, required int index}) {
void removeAppointmentFilterCategorySearchHistory({bool isClear = false, required int index}) {
if (isClear) {
appointmentFilterCategorySearchHistory.clear();
notifyListeners();
@ -1826,8 +1799,7 @@ class AppointmentsVM extends BaseVM {
List<DropValue> appointmentFilterServicesSearchHistory = [];
void removeAppointmentFilterServicesSearchHistory(
{bool isClear = false, required int index}) {
void removeAppointmentFilterServicesSearchHistory({bool isClear = false, required int index}) {
if (isClear) {
appointmentFilterServicesSearchHistory.clear();
notifyListeners();
@ -1868,8 +1840,7 @@ class AppointmentsVM extends BaseVM {
List<DropValue> appointmentFilterItemsSearchHistory = [];
void removeAppointmentFilterItemsSearchHistory(
{bool isClear = false, required int index}) {
void removeAppointmentFilterItemsSearchHistory({bool isClear = false, required int index}) {
if (isClear) {
appointmentFilterItemsSearchHistory.clear();
notifyListeners();
@ -1918,8 +1889,7 @@ class AppointmentsVM extends BaseVM {
notifyListeners();
}
void removeAppointmentFilterCustomerNameSearchHistory(
{bool isClear = false, required int index}) {
void removeAppointmentFilterCustomerNameSearchHistory({bool isClear = false, required int index}) {
if (isClear) {
appointmentFilterCustomerNameSearchHistory.clear();
notifyListeners();
@ -1957,8 +1927,7 @@ class AppointmentsVM extends BaseVM {
List<String> appointmentFilterMobilePhoneSearchHistory = [];
void removeAppointmentFilterMobilePhoneSearchHistory(
{bool isClear = false, required int index}) {
void removeAppointmentFilterMobilePhoneSearchHistory({bool isClear = false, required int index}) {
if (isClear) {
appointmentFilterMobilePhoneSearchHistory.clear();
notifyListeners();
@ -1989,8 +1958,7 @@ class AppointmentsVM extends BaseVM {
List<DropValue> appointmentFilterServiceDeliverySearchHistory = [];
void removeAppointmentFilterServiceDeliverySearchHistory(
{bool isClear = false, required int index}) {
void removeAppointmentFilterServiceDeliverySearchHistory({bool isClear = false, required int index}) {
if (isClear) {
appointmentFilterServiceDeliverySearchHistory.clear();
notifyListeners();
@ -2004,8 +1972,7 @@ class AppointmentsVM extends BaseVM {
notifyListeners();
}
void addAppointmentFilterServiceDeliverySearchHistory(
{required DropValue value}) {
void addAppointmentFilterServiceDeliverySearchHistory({required DropValue value}) {
if (appointmentFilterServiceDeliverySearchHistory.isEmpty) {
updateAppointmentFiltersCounter(appointmentFiltersCounter + 1);
}
@ -2020,8 +1987,7 @@ class AppointmentsVM extends BaseVM {
void updateAppointmentFilterSelectedServiceDeliveryId(SelectionModel id,
{bool isForSearch = false}) async {
if (isForSearch) {
DropValue itemsDrop = serviceDeliveryTypesDropList.firstWhere((
element) => element.id == id.selectedId);
DropValue itemsDrop = serviceDeliveryTypesDropList.firstWhere((element) => element.id == id.selectedId);
if (!ifAlreadyExist(list: appointmentFilterServiceDeliverySearchHistory,
value: itemsDrop)) {
addAppointmentFilterServiceDeliverySearchHistory(value: itemsDrop);
@ -2069,8 +2035,7 @@ class AppointmentsVM extends BaseVM {
notifyListeners();
}
Future<List<AppointmentListModel>> getAppointmentsBasedOnFiltersForCustomer(
{required AppointmentStatusEnum appointmentStatusEnum}) async {
Future<List<AppointmentListModel>> getAppointmentsBasedOnFiltersForCustomer({required AppointmentStatusEnum appointmentStatusEnum}) async {
try {
setState(ViewState.busy);
List<String> providersIdsList = [];
@ -2126,8 +2091,7 @@ class AppointmentsVM extends BaseVM {
String selectedDateForAppointments = "";
Future<List<AppointmentListModel>> getAppointmentsBasedOnFiltersForProviders(
{required int branchID, AppointmentStatusEnum? appointmentStatus}) async {
Future<List<AppointmentListModel>> getAppointmentsBasedOnFiltersForProviders({required int branchID, AppointmentStatusEnum? appointmentStatus}) async {
try {
setState(ViewState.busy);
List<String> servicesIdsList = [];
@ -2183,36 +2147,29 @@ class AppointmentsVM extends BaseVM {
}
}
Future<bool> addProviderToFavorite(
{required int serviceProviderID, required BuildContext context}) async {
Utils.showLoading(context);
Future<bool> addProviderToFavorite({required int serviceProviderID, BuildContext? context}) async {
try {
GenericRespModel genericRespModel = await branchRepo
.addProviderToFavourite(providerID: serviceProviderID);
Utils.showToast(genericRespModel.message.toString());
Utils.hideLoading(context);
GenericRespModel genericRespModel = await branchRepo.addProviderToFavourite(providerID: serviceProviderID);
if (genericRespModel.message != null && genericRespModel.message!.isNotEmpty) {
Utils.showToast(genericRespModel.message!);
}
return genericRespModel.messageStatus == 1;
} catch (e, s) {
logger.e(s);
Utils.hideLoading(context);
Utils.showToast(e.toString());
return false;
}
}
Future<bool> removeProviderFromFavorite(
{required int serviceProviderID, required BuildContext context}) async {
Utils.showLoading(context);
Future<bool> removeProviderFromFavorite({required int serviceProviderID, BuildContext? context}) async {
try {
GenericRespModel genericRespModel = await branchRepo
.removeProviderFromFavourite(providerID: serviceProviderID);
Utils.hideLoading(context);
Utils.showToast(genericRespModel.message.toString());
GenericRespModel genericRespModel = await branchRepo.removeProviderFromFavourite(providerID: serviceProviderID);
if (genericRespModel.message != null && genericRespModel.message!.isNotEmpty) {
Utils.showToast(genericRespModel.message!);
}
return genericRespModel.messageStatus == 1;
} catch (e, s) {
logger.e(s);
Utils.hideLoading(context);
Utils.showToast(e.toString());
return false;
}
@ -2233,6 +2190,25 @@ class AppointmentsVM extends BaseVM {
}
}
Future<bool> removeProviderFromFavourite({required int providerID}) async {
try {
GenericRespModel genericRespModel = await branchRepo.removeProviderFromFavourite(providerID: providerID);
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;
}
}
List<BranchRatingModel> currentBranchReviews = [];
bool isReadMoreEnabled = false;
@ -2246,8 +2222,7 @@ class AppointmentsVM extends BaseVM {
notifyListeners();
}
Future<void> getReviewsByBranchId(
{required int serviceProviderBranchID}) async {
Future<void> getReviewsByBranchId({required int serviceProviderBranchID}) async {
setState(ViewState.busy);
try {
currentBranchReviews = await branchRepo.getBranchRatings(
@ -2262,8 +2237,7 @@ class AppointmentsVM extends BaseVM {
// Chat Related
Future<void> getGeneralChatsListForProvider(
{required BuildContext context}) async {
Future<void> getGeneralChatsListForProvider({required BuildContext context}) async {
try {
// Utils.showLoading(context);
// List<BuyersChatForAdzsModel> buyersChatList = await appointmentRepo.getChatBuyersForAds(adsID: adsID);
@ -2278,8 +2252,7 @@ class AppointmentsVM extends BaseVM {
}
}
void onGeneralChatMessagesButtonPressed(
{required BuildContext context}) async {
void onGeneralChatMessagesButtonPressed({required BuildContext context}) async {
final myUserID = AppState().getUser.data!.userInfo!.userId;
// if (adDetailsModel.isMyAd != null && adDetailsModel.isMyAd!) {
// await getGeneralChatsListForProvider(context: context);

@ -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(
@ -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 = "";
@ -334,6 +388,13 @@ class RequestsVM extends BaseVM {
}
}
// Initialize spare parts condition filters (All, New, Used)
if (sparePartsConditionFilterOptions.isEmpty) {
sparePartsConditionFilterOptions.add(FilterListModel(title: "All", isSelected: true, id: 3)); // All
sparePartsConditionFilterOptions.add(FilterListModel(title: "New Spare Part", isSelected: false, id: 1)); // New
sparePartsConditionFilterOptions.add(FilterListModel(title: "Used Spare Part", 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());

@ -259,6 +259,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 +320,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,4 +1,5 @@
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';
@ -24,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;
@ -91,7 +94,22 @@ class _AdsDetailViewState extends State<AdsDetailView> {
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: [
@ -545,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';
@ -110,20 +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});
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;
@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: [
@ -134,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(
@ -179,7 +301,11 @@ class AdCard extends StatelessWidget {
chipColor: Utils.getChipColorByAdStatus(adDetails.adPostStatus!),
),
],
(adDetails.vehicle!.vehicleTitle ?? "").toText(
// 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),
],
(widget.adDetails.vehicle!.vehicleTitle ?? "").toText(
fontSize: 16,
letterSpacing: -0.64,
height: 25 / 16,
@ -193,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,
@ -209,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,
@ -223,10 +349,8 @@ 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(),
adDetails.createdOn != null
? DateTime.parse(adDetails.createdOn!).getTimeAgo(isArabic: isArabic).toText(color: MyColors.lightTextColor) : const SizedBox(),
(widget.adDetails.vehicle!.cityName ?? "").toText(color: MyColors.lightTextColor),
widget.adDetails.createdOn != null ? DateTime.parse(widget.adDetails.createdOn!).getTimeAgo().toText(color: MyColors.lightTextColor) : const SizedBox(),
],
),
],
@ -239,9 +363,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),
],
),
),
@ -284,6 +408,7 @@ class AdCard extends StatelessWidget {
).toWhiteContainer(
width: double.infinity,
allPading: 12,
isBorderRequired: widget.needBorder,
);
}
}

@ -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,
),
),
),
],
),
),
);
});
}
}

@ -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,
),
],
),

@ -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,

@ -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(

@ -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,67 +50,69 @@ 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: [
LocaleKeys.enterPhoneNumber.tr().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: LocaleKeys.phoneWithoutCountryCode.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();
}
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(
@ -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,
);
},
);
}

@ -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
Loading…
Cancel
Save