diff --git a/assets/images/bn_car_flashing_lights.gif b/assets/images/bn_car_flashing_lights.gif new file mode 100644 index 0000000..9a076f5 Binary files /dev/null and b/assets/images/bn_car_flashing_lights.gif differ diff --git a/assets/langs/ar-SA.json b/assets/langs/ar-SA.json index eb03dca..6d022c1 100644 --- a/assets/langs/ar-SA.json +++ b/assets/langs/ar-SA.json @@ -254,6 +254,9 @@ "sendOTP": "إرسال رمز OTP", "noUserAddedYet": "لم يتم إضافة مستخدم بعد", "assignDealerUser": "تعيين مستخدم تاجر", + "confirmAssignDealerUser": "هل أنت متأكد من رغبتك في تعيين مستخدم التاجر هذا للفرع؟", + "removeDealerUser": "إزالة مستخدم تاجر", + "confirmRemoveDealerUser": "هل أنت متأكد من رغبتك في إزالة مستخدم التاجر هذا من الفرع؟", "createNewUser": "إنشاء مستخدم جديد", "dealerUser": "مستخدم تاجر", "setorEditBranchSchedule": "تعيين أو تحرير جدول الفروع", @@ -590,6 +593,9 @@ "myNearbyBranches": "فروعي القريبة", "myRecentBranches": "فروعي الأخيرة", "favoriteList": "قائمة المفضلة", + "myFavouriteProviders": "مزودي الخدمة المفضلين", + "myFavouriteAds": "إعلاناتي المفضلة", + "noFavouriteAds": "لا توجد إعلانات مفضلة", "noReviewsBranch": "هذا الفرع لا يوجد لديه تعليقات.", "readLess": "اقرأ أقل", "readMoreReviews": "اقرأ المزيد من التقييمات", @@ -885,6 +891,12 @@ "month": "شهر", "currentPackage": "الحزمة الحالية", "forVar": "لمدة", - "branch": "الفرع" + "branch": "الفرع", + "shareAd": "مشاركة الإعلان", + "checkOutThisAd": "تفقد هذه السيارة الرائعة!", + "downloadApp": "تحميل تطبيق موّاتر:", + "androidApp": "أندرويد", + "iosApp": "آيفون", + "pleaseWait": "يرجى الانتظار..." } \ No newline at end of file diff --git a/assets/langs/en-US.json b/assets/langs/en-US.json index 07b3fbd..1c18a5e 100644 --- a/assets/langs/en-US.json +++ b/assets/langs/en-US.json @@ -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", @@ -591,6 +594,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 +889,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..." } \ No newline at end of file diff --git a/lib/classes/consts.dart b/lib/classes/consts.dart index 62570e3..600ebb9 100644 --- a/lib/classes/consts.dart +++ b/lib/classes/consts.dart @@ -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 { }, ); } + diff --git a/lib/config/routes.dart b/lib/config/routes.dart index 4fca131..56307b8 100644 --- a/lib/config/routes.dart +++ b/lib/config/routes.dart @@ -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"; diff --git a/lib/extensions/string_extensions.dart b/lib/extensions/string_extensions.dart index 2d2048f..ce22a58 100644 --- a/lib/extensions/string_extensions.dart +++ b/lib/extensions/string_extensions.dart @@ -12,21 +12,20 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:mc_common_app/generated/locale_keys.g.dart'; extension EmailValidator on String { - Widget toText( - {Color? color, - bool isBold = false, - double? fontSize, - bool isUnderLine = false, - bool isItalic = false, - TextDecoration? textDecoration, - double letterSpacing = 0, - TextAlign? textAlign, - FontWeight? fontWeight, - double? height, - Color? decorationColor, - TextOverflow? overflow, - bool isUpperCase = false, - int? maxLines}) { + Widget toText({Color? color, + bool isBold = false, + double? fontSize, + bool isUnderLine = false, + bool isItalic = false, + TextDecoration? textDecoration, + double letterSpacing = 0, + TextAlign? textAlign, + FontWeight? fontWeight, + double? height, + Color? decorationColor, + TextOverflow? overflow, + bool isUpperCase = false, + int? maxLines}) { bool isArabic = AppState().postParamsObject!.languageID == 1; // arabic -> 1 , English -> 2 return AutoSizeText( isUpperCase ? toUpperCase() : this, @@ -290,6 +289,8 @@ extension AppointmentStatusToInt on AppointmentStatusEnum { case AppointmentStatusEnum.upcoming: return 11; + case AppointmentStatusEnum.served: + return 12; case AppointmentStatusEnum.allAppointments: return 0; @@ -386,6 +387,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; } diff --git a/lib/generated/codegen_loader.g.dart b/lib/generated/codegen_loader.g.dart index 420938e..3d10b93 100644 --- a/lib/generated/codegen_loader.g.dart +++ b/lib/generated/codegen_loader.g.dart @@ -606,6 +606,9 @@ class CodegenLoader extends AssetLoader{ "myNearbyBranches": "فروعي القريبة", "myRecentBranches": "فروعي الأخيرة", "favoriteList": "قائمة المفضلة", + "myFavouriteProviders": "مزودي الخدمة المفضلين", + "myFavouriteAds": "إعلاناتي المفضلة", + "noFavouriteAds": "لا توجد إعلانات مفضلة", "noReviewsBranch": "هذا الفرع لا يوجد لديه تعليقات.", "readLess": "اقرأ أقل", "readMoreReviews": "اقرأ المزيد من التقييمات", @@ -833,7 +836,7 @@ class CodegenLoader extends AssetLoader{ "specialRequestsChats": "دردشات الطلبات الخاصة", "selectDeliveryOption": "اختر خيار التوصيل", "deliveryOptions": "خيارات التوصيل", - "selfPickup": "الاستلام الذاتي", + "selfPickup": "استلام ذاتي", "updateYourLocationInfo": "قم بتحديث معلومات موقعك", "initiateSelfPickup": "بدء الاستلام الذاتي", "selfPickupStatus": "حالة الاستلام الذاتي", @@ -841,43 +844,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 +904,13 @@ class CodegenLoader extends AssetLoader{ "month": "شهر", "currentPackage": "الحزمة الحالية", "forVar": "لمدة", - "branch": "الفرع" + "branch": "الفرع", + "shareAd": "مشاركة الإعلان", + "checkOutThisAd": "تفقد هذه السيارة الرائعة!", + "downloadApp": "تحميل تطبيق موّاتر:", + "androidApp": "أندرويد", + "iosApp": "آيفون", + "pleaseWait": "يرجى الانتظار..." }; static const Map _en_US = { "firstTimeLogIn": "First Time Log In", @@ -1496,6 +1505,9 @@ static const Map _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 +1776,12 @@ static const Map _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 +1797,16 @@ static const Map _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> mapLocales = {"ar_SA": _ar_SA, "en_US": _en_US}; } diff --git a/lib/generated/locale_keys.g.dart b/lib/generated/locale_keys.g.dart index 3ed6164..09bafd8 100644 --- a/lib/generated/locale_keys.g.dart +++ b/lib/generated/locale_keys.g.dart @@ -2,7 +2,7 @@ // ignore_for_file: constant_identifier_names -abstract class LocaleKeys { +abstract class LocaleKeys { static const firstTimeLogIn = 'firstTimeLogIn'; static const signUp = 'signUp'; static const changeMobile = 'changeMobile'; @@ -49,7 +49,7 @@ abstract class LocaleKeys { static const emailChangedSuccessfully = 'emailChangedSuccessfully'; static const passwordIsUpdated = 'passwordIsUpdated'; static const passwordShouldContains = 'passwordShouldContains'; - static const successfullyRegistered = ' successfullyRegistered'; + static const successfullyRegistered = ' successfullyRegistered'; static const pleaseEnterSamePassword = 'pleaseEnterSamePassword'; static const firstNameMandatory = 'firstNameMandatory'; static const surnameNameMandatory = 'surnameNameMandatory'; @@ -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'; @@ -571,6 +574,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 +843,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 +853,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 +873,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'; } diff --git a/lib/models/advertisment_models/ad_details_model.dart b/lib/models/advertisment_models/ad_details_model.dart index 8cafcf9..c89dbdc 100644 --- a/lib/models/advertisment_models/ad_details_model.dart +++ b/lib/models/advertisment_models/ad_details_model.dart @@ -45,6 +45,7 @@ class AdDetailsModel { AdReserveStatus? adReserveStatus; bool? isMyAd; bool? isReservedByMe; + bool? isAdFavorite; String? phoneNo; String? whatsAppNo; String? adOwnerName; @@ -93,6 +94,7 @@ class AdDetailsModel { this.adReserveStatus, this.isMyAd, this.isReservedByMe, + this.isAdFavorite, this.phoneNo, this.whatsAppNo, this.adOwnerName, @@ -161,6 +163,7 @@ class AdDetailsModel { createdByRoleEnum = (json['createdByRole'] as int).toCreatedByRoleEnum(); isMyAd = isMyAds; isReservedByMe = false; + isAdFavorite = json['isFavourite'] ?? false; totalItemsCount = totalItems; } } diff --git a/lib/models/general_models/enums_model.dart b/lib/models/general_models/enums_model.dart index e46372f..5413d41 100644 --- a/lib/models/general_models/enums_model.dart +++ b/lib/models/general_models/enums_model.dart @@ -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"], diff --git a/lib/repositories/ads_repo.dart b/lib/repositories/ads_repo.dart index e4455a5..4d0a8ea 100644 --- a/lib/repositories/ads_repo.dart +++ b/lib/repositories/ads_repo.dart @@ -57,6 +57,10 @@ abstract class AdsRepo { Future> getMyAds(); + Future> getFavouriteAds(); + + Future toggleAdFavorite({required int adsID, required bool isFavorite}); + Future getAdBankingAccountInfo({required int adId}); Future updateAdStatus({required int adId, required AdPostStatus adStatusToUpdate, String? comment}); @@ -510,6 +514,38 @@ class AdsRepoImp implements AdsRepo { return vehicleAdsDetails; } + @override + Future> 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 favouriteAdsDetails = List.generate(adsGenericModel.data.length, (index) => AdDetailsModel.fromJson(adsGenericModel.data[index], false, adsGenericModel.totalItemsCount ?? 1)); + return favouriteAdsDetails; + } + + @override + Future 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 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, ); diff --git a/lib/services/payments_service.dart b/lib/services/payments_service.dart index 95ecb61..b6caaa9 100644 --- a/lib/services/payments_service.dart +++ b/lib/services/payments_service.dart @@ -15,6 +15,7 @@ abstract class PaymentService { required PaymentTypes paymentType, required Function() onSuccess, required Function() onFailure, + Function()? onExit, }); Future 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); diff --git a/lib/utils/enums.dart b/lib/utils/enums.dart index 6806c5a..dcc99cd 100644 --- a/lib/utils/enums.dart +++ b/lib/utils/enums.dart @@ -178,6 +178,7 @@ enum AppointmentStatusEnum { today, past, upcoming, + served, } enum AppointmentPaymentStatusEnum { diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart index ca9d93f..d84c4f9 100644 --- a/lib/utils/utils.dart +++ b/lib/utils/utils.dart @@ -36,7 +36,12 @@ class Utils { toastification.show( type: ToastificationType.info, style: ToastificationStyle.minimal, - title: Text(message), + title: Text( + + message, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), autoCloseDuration: const Duration(seconds: 2), alignment: Alignment.bottomCenter, showProgressBar: false, diff --git a/lib/view_models/ad_view_model.dart b/lib/view_models/ad_view_model.dart index 06479de..3426712 100644 --- a/lib/view_models/ad_view_model.dart +++ b/lib/view_models/ad_view_model.dart @@ -89,6 +89,7 @@ class AdVM extends BaseVM { AdDetailsModel? previousAdDetails; List myDraftAds = []; + List favouriteAds = []; List exploreAds = []; List exploreAdsFilteredList = []; List myAdsFilteredList = []; @@ -337,6 +338,25 @@ class AdVM extends BaseVM { return true; } + Future 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 getExploreAds() async { pageIndexForExploreAds = 1; hasMoreDataForExploreAds = true; @@ -364,6 +384,18 @@ class AdVM extends BaseVM { } } + Future getFavouriteAds() async { + try { + setState(ViewState.busy); + favouriteAds = await adsRepo.getFavouriteAds(); + notifyListeners(); + setState(ViewState.idle); + } catch (e) { + setState(ViewState.idle); + logger.e(e.toString()); + } + } + Future fetchMoreDraftAds() async { if (isLoadingMore) return; hasMoreDataForMyDraftsAds = true; diff --git a/lib/view_models/appointments_view_model.dart b/lib/view_models/appointments_view_model.dart index fd88b0a..1c3dd97 100644 --- a/lib/view_models/appointments_view_model.dart +++ b/lib/view_models/appointments_view_model.dart @@ -2183,36 +2183,29 @@ class AppointmentsVM extends BaseVM { } } - Future addProviderToFavorite( - {required int serviceProviderID, required BuildContext context}) async { - Utils.showLoading(context); + Future 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 removeProviderFromFavorite( - {required int serviceProviderID, required BuildContext context}) async { - Utils.showLoading(context); - + Future 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 +2226,25 @@ class AppointmentsVM extends BaseVM { } } + Future 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 currentBranchReviews = []; bool isReadMoreEnabled = false; diff --git a/lib/view_models/payment_view_model.dart b/lib/view_models/payment_view_model.dart index bcb4030..41b0717 100644 --- a/lib/view_models/payment_view_model.dart +++ b/lib/view_models/payment_view_model.dart @@ -76,14 +76,14 @@ class PaymentVM extends ChangeNotifier { Future verifyPayments( {required BuildContext context, required int id, List? 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 placeThePayment({required PaymentTypes paymentTypeEnum, required BuildContext context}) async { + Future 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 onVisaCardSelected(BuildContext context, PaymentTypes paymentType) async { + Future 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; } } diff --git a/lib/views/advertisement/ads_detail_view/ads_detail_view.dart b/lib/views/advertisement/ads_detail_view/ads_detail_view.dart index 8efc736..11a34a0 100644 --- a/lib/views/advertisement/ads_detail_view/ads_detail_view.dart +++ b/lib/views/advertisement/ads_detail_view/ads_detail_view.dart @@ -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 { 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: [ @@ -529,3 +547,179 @@ class _AdsDetailViewState extends State { ); } } + +class FavoriteAdButton extends StatefulWidget { + final AdDetailsModel adDetails; + + const FavoriteAdButton({super.key, required this.adDetails}); + + @override + State createState() => _FavoriteAdButtonState(); +} + +class _FavoriteAdButtonState extends State with SingleTickerProviderStateMixin { + bool isLoading = false; + late bool isFavorite; + late AnimationController _animationController; + late Animation _scaleAnimation; + + @override + void initState() { + super.initState(); + isFavorite = widget.adDetails.isAdFavorite ?? false; + + _animationController = AnimationController( + duration: const Duration(milliseconds: 300), + vsync: this, + ); + + _scaleAnimation = Tween(begin: 1.0, end: 1.3).animate( + CurvedAnimation(parent: _animationController, curve: Curves.easeInOut), + ); + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } + + Future toggleFavorite() async { + if (isLoading) return; + + setState(() { + isLoading = true; + }); + + _animationController.forward().then((_) => _animationController.reverse()); + + bool success = await context.read().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(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, + ), + ), + ), + ); + } +} diff --git a/lib/views/advertisement/bottom_sheets/ad_duration_selection_sheet.dart b/lib/views/advertisement/bottom_sheets/ad_duration_selection_sheet.dart index 26b26bf..394d147 100644 --- a/lib/views/advertisement/bottom_sheets/ad_duration_selection_sheet.dart +++ b/lib/views/advertisement/bottom_sheets/ad_duration_selection_sheet.dart @@ -125,6 +125,8 @@ class AdDurationSelectionSheet extends StatelessWidget { isBorderRequired: adDuration.id == adVM.vehicleAdDurationId.selectedId, ), ).onPress(() { + + log("isFromExtendAd: $isFromExtendAd"); if (isFromExtendAd) { adVM.updateVehicleExtendAdDurationId( SelectionModel( diff --git a/lib/views/advertisement/components/ads_list_widget.dart b/lib/views/advertisement/components/ads_list_widget.dart index 3317ded..8689859 100644 --- a/lib/views/advertisement/components/ads_list_widget.dart +++ b/lib/views/advertisement/components/ads_list_widget.dart @@ -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 createState() => _AdCardState(); +} + +class _AdCardState extends State { + 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,98 @@ 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( + borderRadius: BorderRadius.circular(8), + 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( @@ -153,12 +276,12 @@ class AdCard extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start, children: [ - if (isAdsFragment && adDetails.adPostStatus! == AdPostStatus.sold && adDetails.statuslabel != null && adDetails.statuslabel!.isNotEmpty) ...[ - Utils.statusContainerChip(text: adDetails.statuslabel ?? "", chipColor: Utils.getChipColorByAdStatus(adDetails.adPostStatus!)), - ] else if (isAdsFragment && !context.read().isExploreAdsTapped && adDetails.statuslabel != null && adDetails.statuslabel!.isNotEmpty) ...[ - Utils.statusContainerChip(text: adDetails.statuslabel ?? "", chipColor: Utils.getChipColorByAdStatus(adDetails.adPostStatus!)), + if (isAdsFragment && widget.adDetails.adPostStatus! == AdPostStatus.sold && widget.adDetails.statuslabel != null && widget.adDetails.statuslabel!.isNotEmpty) ...[ + Utils.statusContainerChip(text: widget.adDetails.statuslabel ?? "", chipColor: Utils.getChipColorByAdStatus(widget.adDetails.adPostStatus!)), + ] else if (isAdsFragment && !context.read().isExploreAdsTapped && widget.adDetails.statuslabel != null && widget.adDetails.statuslabel!.isNotEmpty) ...[ + Utils.statusContainerChip(text: widget.adDetails.statuslabel ?? "", chipColor: Utils.getChipColorByAdStatus(widget.adDetails.adPostStatus!)), ], - (adDetails.vehicle!.vehicleTitle ?? "").toText( + (widget.adDetails.vehicle!.vehicleTitle ?? "").toText( fontSize: 16, letterSpacing: -0.64, height: 25 / 16, @@ -172,7 +295,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, @@ -188,7 +311,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, @@ -202,8 +325,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(), + (widget.adDetails.vehicle!.cityName ?? "").toText(color: MyColors.lightTextColor), + widget.adDetails.createdOn != null ? DateTime.parse(widget.adDetails.createdOn!).getTimeAgo().toText(color: MyColors.lightTextColor) : const SizedBox(), ], ), ], @@ -216,9 +339,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), ], ), ), @@ -261,6 +384,7 @@ class AdCard extends StatelessWidget { ).toWhiteContainer( width: double.infinity, allPading: 12, + isBorderRequired: widget.needBorder, ); } } diff --git a/lib/views/advertisement/favourite_ads_view.dart b/lib/views/advertisement/favourite_ads_view.dart new file mode 100644 index 0000000..fa8eee0 --- /dev/null +++ b/lib/views/advertisement/favourite_ads_view.dart @@ -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 createState() => _FavouriteAdsViewState(); +} + +class _FavouriteAdsViewState extends State { + int selectedBrandId = 0; + + @override + void initState() { + scheduleMicrotask(() async { + final adVM = context.read(); + await adVM.getFavouriteAds(); + // Load filter options if not already loaded + if (adVM.exploreAdsFilterOptions.isEmpty) { + await adVM.populateAdsFilterList(); + } + }); + super.initState(); + } + + List 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, + ), + ), + ), + ], + ), + ), + ); + }); + } +} diff --git a/lib/views/advertisement/select_ad_type_view.dart b/lib/views/advertisement/select_ad_type_view.dart index 06b5602..5961765 100644 --- a/lib/views/advertisement/select_ad_type_view.dart +++ b/lib/views/advertisement/select_ad_type_view.dart @@ -78,7 +78,7 @@ class SelectAdTypeView extends StatelessWidget { onTap: () async { adVM.updateSelectionVehicleTypeId(SelectionModel(selectedId: vehicleTypeModel.id!, selectedOption: vehicleTypeModel.vehicleTypeName ?? "", errorValue: "")); if (adVM.isAdEditEnabled) { - adVM.autoFillSelectedVehicleAdsDetails(); + adVM.autoFillSelectedVehicleAdsDetails(context); } if (AppState().currentAppType == AppType.provider) { if (isFromExtendAd && !adVM.isAdEditEnabled) { diff --git a/lib/views/payments/payment_methods_view.dart b/lib/views/payments/payment_methods_view.dart index d7b72db..870e69f 100644 --- a/lib/views/payments/payment_methods_view.dart +++ b/lib/views/payments/payment_methods_view.dart @@ -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 createState() => _PaymentMethodsViewState(); +} + +class _PaymentMethodsViewState extends State { + @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().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(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().onContinuePressed(context, paymentType: paymentType); - } - }, - backgroundColor: MyColors.darkPrimaryColor, - ), + 24.height, + LocaleKeys.pleaseWait.tr().toText( + fontSize: 16, + color: MyColors.darkTextColor, ), ], ), diff --git a/lib/views/setting_options/setting_options_more.dart b/lib/views/setting_options/setting_options_more.dart index d741835..62483dd 100644 --- a/lib/views/setting_options/setting_options_more.dart +++ b/lib/views/setting_options/setting_options_more.dart @@ -83,9 +83,9 @@ class _SettingOptionsMoreState extends State { 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 { 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, diff --git a/lib/views/splash/splash_page.dart b/lib/views/splash/splash_page.dart index e99dd8e..c1808fe 100644 --- a/lib/views/splash/splash_page.dart +++ b/lib/views/splash/splash_page.dart @@ -15,11 +15,31 @@ class SplashPage extends StatefulWidget { } class SplashPageState extends State 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 @@ -68,12 +88,22 @@ class SplashPageState extends State 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( diff --git a/lib/widgets/common_widgets/add_phone_num_wiget.dart b/lib/widgets/common_widgets/add_phone_num_wiget.dart index 8736f25..3762f80 100644 --- a/lib/widgets/common_widgets/add_phone_num_wiget.dart +++ b/lib/widgets/common_widgets/add_phone_num_wiget.dart @@ -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 { 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 { ); } - void getCountry(BuildContext context) { - List 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 { return isValid; } } - -class CustomBottomPicker extends StatelessWidget { - final List 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 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, - ); - }, - ); -} diff --git a/lib/widgets/common_widgets/provider_details_card.dart b/lib/widgets/common_widgets/provider_details_card.dart index 1167613..c0a2dd2 100644 --- a/lib/widgets/common_widgets/provider_details_card.dart +++ b/lib/widgets/common_widgets/provider_details_card.dart @@ -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), + ], + ), + ], + ), + ), + ], + ), ], ), ),