diff --git a/assets/langs/ar-SA.json b/assets/langs/ar-SA.json index c3a7664..4859c6a 100644 --- a/assets/langs/ar-SA.json +++ b/assets/langs/ar-SA.json @@ -12,6 +12,7 @@ "verify": "تحقق", "verified": "تم التحقق", "signOut": "تسجيل خروج", + "accessDenied": "الوصول مرفوض", "enterEmail": "ادخل الايميل", "enterNewEmail": "ادخل ايميل جديد", "enterCurrentPassword": "كلمة المرور الحاليه", @@ -63,6 +64,8 @@ "imageUploaded": "تم تحميل الصوره", "fileLarger": "1KB حجم الملف اكبر من", "account": "الحساب", + "role": "الدور", + "userType": "نوع المستخدم", "edit": "تعديل", "imageDeleted": "تم حذف الصوره", "dashboard": "الصفحه الرئيسيه", @@ -383,6 +386,7 @@ "appointmentBookedSuccessfully": "تم حجز موعدك بنجاح", "appointmentStatusUpdated": "تم تحديث حالة الموعد", "paymentStatusUpdated": "تم تحديث حالة الدفع", + "serviceStatusUpdated": "تم تحديث حالة الخدمة", "selectOneSubService": "يرجى اختيار خدمة فرعية واحدة على الأقل", "chargesBreakdown": "تفاصيل الرسوم", "services": "الخدمات", @@ -662,6 +666,7 @@ "customerCarePrompt": "هل لديك أي استفسار؟ نحن هنا لخدمتك. انقر هنا للتحدث مع خدمة العملاء لدينا الآن.", "callNow": "اتصل الآن", "condition": "الحالة", + "newCondition": "جديد", "servicesNotAvailableHomeLocation": "بعض الخدمات غير متوفرة في الموقع المنزلي.", "additionalChargesNotice": "هذه الرسوم إضافية على رسوم الخدمة الفعلية. قد تختلف الرسوم للعناصر الثقيلة.", "selectServiceLocation": "اختر موقع الخدمة", @@ -738,6 +743,7 @@ "markAdAsSoldDesc": "سيتم وضع علامة 'تم البيع' على هذا الإعلان، ولن يتمكن المستخدمون الآخرون من التواصل معك.", "ownerInformation": "معلومات المالك", "acceptedRequests": "الطلبات المقبولة", + "specialRequests": "الطلبات الخاصة", "specialRequestChat": "دردشة الطلب الخاص", "sparePartRequestChat": "دردشة طلب قطع الغيار", "specialCarRequestChat": "دردشة طلب السيارة الخاصة", @@ -905,5 +911,8 @@ "androidApp": "أندرويد", "iosApp": "آيفون", "pleaseWait": "يرجى الانتظار...", - "numberOfShipping": "عدد الشحنات" + "numberOfShipping": "عدد الشحنات", + "loading": "جار التحميل...", + "pleaseSelectAccidentFree": "يرجى تحديد ما إذا كانت المركبة خالية من الحوادث", + "pleaseSelectMileageIncrease": "يرجى تحديد ما إذا كان يمكن زيادة المسافة المقطوعة" } \ No newline at end of file diff --git a/assets/langs/en-US.json b/assets/langs/en-US.json index 1414964..4a178f5 100644 --- a/assets/langs/en-US.json +++ b/assets/langs/en-US.json @@ -12,6 +12,7 @@ "verify": "Verify", "verified": "Verified", "signOut": "Sign Out", + "accessDenied": "Access Denied", "enterEmail": "Enter Email", "enterNewEmail": "Enter New Email", "enterNewPassword": "Enter New Password", @@ -22,6 +23,9 @@ "enterNewPhoneNumber": "Enter New Phone Number", "enterPhoneNumber": "Enter Phone Number", "continu": "Continue", + "loading": "Loading...", + "pleaseSelectAccidentFree": "Please select if the vehicle is accident free", + "pleaseSelectMileageIncrease": "Please select if mileage could increase", "confirmPassword": "Confirm Password", "createPassword": "Create Password", "email": "Email", @@ -63,6 +67,8 @@ "imageUploaded": "Image is uploaded", "fileLarger": "File is larger then 1KB", "account": "Account", + "role": "Role", + "userType": "User Type", "edit": "EDIT", "imageDeleted": "Image is Deleted", "dashboard": "Dashboard/Main Page", @@ -383,6 +389,7 @@ "appointmentBookedSuccessfully": "Your appointment has been booked successfully", "appointmentStatusUpdated": "Appointment Status Updated", "paymentStatusUpdated": "Payment Status Updated", + "serviceStatusUpdated": "Service Status Updated", "selectOneSubService": "Please select at least one sub service", "chargesBreakdown": "Charges Breakdown", "services": "Services", @@ -426,7 +433,7 @@ "usedSparePart": "Used Spare Part", "hasYourCarHadAccident": "Has your car had any accident?", "odometerReading": "Odometer Reading (Km)", - "canMileageIncrease": "Can the mileage be increased when checking?", + "canMileageIncrease": "Mileage Could Increase?", "currentlyInUse": "Currently in Use", "demand": "Demand", "adDurationExpired": "Your Ad Duration time is over", @@ -663,6 +670,7 @@ "customerCarePrompt": "Got any question? We are here to service. Click here to talk to our customer care right now.", "callNow": "Call Now", "condition": "Condition", + "newCondition": "New", "servicesNotAvailableHomeLocation": "This location will be same for all services", "additionalChargesNotice": "These charges are additional to the actual service charges. For heavy items the charges may vary.", "selectServiceLocation": "Select Service Location", @@ -733,6 +741,7 @@ "ownerInformation": "Owner Information", "noItemsToShow": "There are no Items no show.", "acceptedRequests": "Accepted Requests", + "specialRequests": "Special Requests", "specialRequestChat": "Special Request Chat", "sparePartRequestChat": "Spare Part Request Chat", "specialCarRequestChat": "Special Car Request Chat", diff --git a/lib/generated/codegen_loader.g.dart b/lib/generated/codegen_loader.g.dart index 93492d6..dc1691b 100644 --- a/lib/generated/codegen_loader.g.dart +++ b/lib/generated/codegen_loader.g.dart @@ -28,6 +28,7 @@ class CodegenLoader extends AssetLoader{ "verify": "تحقق", "verified": "تم التحقق", "signOut": "تسجيل خروج", + "accessDenied": "الوصول مرفوض", "enterEmail": "ادخل الايميل", "enterNewEmail": "ادخل ايميل جديد", "enterCurrentPassword": "كلمة المرور الحاليه", @@ -79,6 +80,8 @@ class CodegenLoader extends AssetLoader{ "imageUploaded": "تم تحميل الصوره", "fileLarger": "1KB حجم الملف اكبر من", "account": "الحساب", + "role": "الدور", + "userType": "نوع المستخدم", "edit": "تعديل", "imageDeleted": "تم حذف الصوره", "dashboard": "الصفحه الرئيسيه", @@ -678,6 +681,7 @@ class CodegenLoader extends AssetLoader{ "customerCarePrompt": "هل لديك أي استفسار؟ نحن هنا لخدمتك. انقر هنا للتحدث مع خدمة العملاء لدينا الآن.", "callNow": "اتصل الآن", "condition": "الحالة", + "newCondition": "جديد", "servicesNotAvailableHomeLocation": "بعض الخدمات غير متوفرة في الموقع المنزلي.", "additionalChargesNotice": "هذه الرسوم إضافية على رسوم الخدمة الفعلية. قد تختلف الرسوم للعناصر الثقيلة.", "selectServiceLocation": "اختر موقع الخدمة", @@ -754,6 +758,7 @@ class CodegenLoader extends AssetLoader{ "markAdAsSoldDesc": "سيتم وضع علامة 'تم البيع' على هذا الإعلان، ولن يتمكن المستخدمون الآخرون من التواصل معك.", "ownerInformation": "معلومات المالك", "acceptedRequests": "الطلبات المقبولة", + "specialRequests": "الطلبات الخاصة", "specialRequestChat": "دردشة الطلب الخاص", "sparePartRequestChat": "دردشة طلب قطع الغيار", "specialCarRequestChat": "دردشة طلب السيارة الخاصة", @@ -921,7 +926,10 @@ class CodegenLoader extends AssetLoader{ "androidApp": "أندرويد", "iosApp": "آيفون", "pleaseWait": "يرجى الانتظار...", - "numberOfShipping": "عدد الشحنات" + "numberOfShipping": "عدد الشحنات", + "loading": "جار التحميل...", + "pleaseSelectAccidentFree": "يرجى تحديد ما إذا كانت المركبة خالية من الحوادث", + "pleaseSelectMileageIncrease": "يرجى تحديد ما إذا كان يمكن زيادة المسافة المقطوعة" }; static const Map _en_US = { "firstTimeLogIn": "First Time Log In", @@ -937,6 +945,7 @@ static const Map _en_US = { "verify": "Verify", "verified": "Verified", "signOut": "Sign Out", + "accessDenied": "Access Denied", "enterEmail": "Enter Email", "enterNewEmail": "Enter New Email", "enterNewPassword": "Enter New Password", @@ -947,6 +956,9 @@ static const Map _en_US = { "enterNewPhoneNumber": "Enter New Phone Number", "enterPhoneNumber": "Enter Phone Number", "continu": "Continue", + "loading": "Loading...", + "pleaseSelectAccidentFree": "Please select if the vehicle is accident free", + "pleaseSelectMileageIncrease": "Please select if mileage can be increased", "confirmPassword": "Confirm Password", "createPassword": "Create Password", "email": "Email", @@ -988,6 +1000,8 @@ static const Map _en_US = { "imageUploaded": "Image is uploaded", "fileLarger": "File is larger then 1KB", "account": "Account", + "role": "Role", + "userType": "User Type", "edit": "EDIT", "imageDeleted": "Image is Deleted", "dashboard": "Dashboard/Main Page", @@ -1399,7 +1413,7 @@ static const Map _en_US = { "cancelReservation": "Cancel Reservation", "reasonForCancellation": "Reason for Cancellation", "doWantCancelReservation": "Do you want to cancel the reservation", - "yourAdVisibletoBuy.": "Your ad reservation will be cancelled and this ad will be again visible to everyone to buy", + "yourAdVisibletoBuy": "Your ad reservation will be cancelled and this ad will be again visible to everyone to buy", "completeDeal": "Complete Deal", "uploadBankReceipt": "Upload Bank Receipt", "notesForAdmin": "Notes For Admin", @@ -1588,6 +1602,7 @@ static const Map _en_US = { "customerCarePrompt": "Got any question? We are here to service. Click here to talk to our customer care right now.", "callNow": "Call Now", "condition": "Condition", + "newCondition": "New", "servicesNotAvailableHomeLocation": "This location will be same for all services", "additionalChargesNotice": "These charges are additional to the actual service charges. For heavy items the charges may vary.", "selectServiceLocation": "Select Service Location", @@ -1658,6 +1673,7 @@ static const Map _en_US = { "ownerInformation": "Owner Information", "noItemsToShow": "There are no Items no show.", "acceptedRequests": "Accepted Requests", + "specialRequests": "Special Requests", "specialRequestChat": "Special Request Chat", "sparePartRequestChat": "Spare Part Request Chat", "specialCarRequestChat": "Special Car Request Chat", diff --git a/lib/generated/locale_keys.g.dart b/lib/generated/locale_keys.g.dart index 678714e..3aedaa9 100644 --- a/lib/generated/locale_keys.g.dart +++ b/lib/generated/locale_keys.g.dart @@ -16,6 +16,7 @@ abstract class LocaleKeys { static const verify = 'verify'; static const verified = 'verified'; static const signOut = 'signOut'; + static const accessDenied = 'accessDenied'; static const enterEmail = 'enterEmail'; static const enterNewEmail = 'enterNewEmail'; static const enterCurrentPassword = 'enterCurrentPassword'; @@ -67,6 +68,8 @@ abstract class LocaleKeys { static const imageUploaded = 'imageUploaded'; static const fileLarger = 'fileLarger'; static const account = 'account'; + static const role = 'role'; + static const userType = 'userType'; static const edit = 'edit'; static const imageDeleted = 'imageDeleted'; static const dashboard = 'dashboard'; @@ -364,6 +367,7 @@ abstract class LocaleKeys { static const appointmentBookedSuccessfully = 'appointmentBookedSuccessfully'; static const appointmentStatusUpdated = 'appointmentStatusUpdated'; static const paymentStatusUpdated = 'paymentStatusUpdated'; + static const serviceStatusUpdated = 'serviceStatusUpdated'; static const selectOneSubService = 'selectOneSubService'; static const chargesBreakdown = 'chargesBreakdown'; static const services = 'services'; @@ -643,6 +647,7 @@ abstract class LocaleKeys { static const customerCarePrompt = 'customerCarePrompt'; static const callNow = 'callNow'; static const condition = 'condition'; + static const newCondition = 'newCondition'; static const servicesNotAvailableHomeLocation = 'servicesNotAvailableHomeLocation'; static const additionalChargesNotice = 'additionalChargesNotice'; static const selectServiceLocation = 'selectServiceLocation'; @@ -719,6 +724,7 @@ abstract class LocaleKeys { static const markAdAsSoldDesc = 'markAdAsSoldDesc'; static const ownerInformation = 'ownerInformation'; static const acceptedRequests = 'acceptedRequests'; + static const specialRequests = 'specialRequests'; static const specialRequestChat = 'specialRequestChat'; static const sparePartRequestChat = 'sparePartRequestChat'; static const specialCarRequestChat = 'specialCarRequestChat'; @@ -887,4 +893,8 @@ abstract class LocaleKeys { static const iosApp = 'iosApp'; static const pleaseWait = 'pleaseWait'; static const numberOfShipping = 'numberOfShipping'; + static const loading = 'loading'; + static const pleaseSelectAccidentFree = 'pleaseSelectAccidentFree'; + static const pleaseSelectMileageIncrease = 'pleaseSelectMileageIncrease'; + } diff --git a/lib/models/advertisment_models/ad_details_model.dart b/lib/models/advertisment_models/ad_details_model.dart index ced36a7..9a4ebc5 100644 --- a/lib/models/advertisment_models/ad_details_model.dart +++ b/lib/models/advertisment_models/ad_details_model.dart @@ -43,12 +43,13 @@ class AdDetailsModel { String? modifiedOn; AdPostStatus? adPostStatus; AdReserveStatus? adReserveStatus; - int? adsReserveId; // Reserve ID from AdsReserve_Get API + int? adsReserveId; // Reserve ID from AdsReserve_Get API bool? isReservable; String? reserveCustomerMessage; bool? isMyAd; bool? isReservedByMe; bool? isAdFavorite; + int? favouriteAdID; // Favorite record ID (used when unfavoriting) String? phoneNo; String? whatsAppNo; String? adOwnerName; @@ -101,6 +102,7 @@ class AdDetailsModel { this.isMyAd, this.isReservedByMe, this.isAdFavorite, + this.favouriteAdID, this.phoneNo, this.whatsAppNo, this.adOwnerName, @@ -172,6 +174,7 @@ class AdDetailsModel { isMyAd = isMyAds; isReservedByMe = false; isAdFavorite = json['isFavourite'] ?? false; + favouriteAdID = json['favouriteAdID']; totalItemsCount = totalItems; } } diff --git a/lib/models/user_models/user.dart b/lib/models/user_models/user.dart index 7c91069..cbdd4c9 100644 --- a/lib/models/user_models/user.dart +++ b/lib/models/user_models/user.dart @@ -82,6 +82,7 @@ class UserInfo { this.userImageUrl, this.roleId, this.roleName, + this.roleNameN, this.genderID, this.genderName, this.isEmailVerified, @@ -115,6 +116,7 @@ class UserInfo { dynamic userImageUrl; int? roleId; String? roleName; + String? roleNameN; // Arabic role name bool? isEmailVerified; List? serviceProviderBranch; bool? isVerified; @@ -152,6 +154,7 @@ class UserInfo { userImageUrl = json["userImageUrl"]; roleId = json["roleID"]; roleName = json["roleName"]; + roleNameN = json["roleNameN"]; isEmailVerified = json["isEmailVerified"]; serviceProviderBranch = json["serviceProviderBranch"] == null ? null : List.from(json["serviceProviderBranch"].map((x) => x)); isVerified = json["isVerified"]; @@ -202,6 +205,7 @@ class UserInfo { "userImageUrl": userImageUrl, "roleID": roleId, "roleName": roleName, + "roleNameN": roleNameN, "isEmailVerified": isEmailVerified, "serviceProviderBranch": serviceProviderBranch == null ? null : List.from(serviceProviderBranch!.map((x) => x)), "isVerified": isVerified, diff --git a/lib/services/firebase_service.dart b/lib/services/firebase_service.dart index 63fb768..a714024 100644 --- a/lib/services/firebase_service.dart +++ b/lib/services/firebase_service.dart @@ -13,7 +13,9 @@ import 'package:mc_common_app/config/routes.dart'; import 'package:mc_common_app/main.dart'; import 'package:mc_common_app/services/chat_navigation_service.dart'; import 'package:mc_common_app/utils/navigator.dart'; +import 'package:mc_common_app/view_models/chat_view_model.dart'; import 'package:permission_handler/permission_handler.dart'; +import 'package:provider/provider.dart'; abstract class FirebaseMessagingService { Future getFirebaseToken(); @@ -53,6 +55,25 @@ class FirebaseMessagingServiceImp implements FirebaseMessagingService { } }); + // Create high-priority notification channel for Android (required for terminated state) + if (Platform.isAndroid) { + const AndroidNotificationChannel channel = AndroidNotificationChannel( + 'mowater_chat_channel', // More descriptive channel ID + 'Chat Notifications', + description: 'Notifications for chat messages', + importance: Importance.max, // Changed to max for Android 14 + playSound: true, + enableVibration: true, + showBadge: true, + ); + + await flutterLocalNotifications + .resolvePlatformSpecificImplementation() + ?.createNotificationChannel(channel); + + log("✅ Android notification channel created: ${channel.id}"); + } + // initialise the plugin. app_icon needs to be a added as a drawable resource to the Android head project const AndroidInitializationSettings initializationSettingsAndroid = AndroidInitializationSettings('ic_launcher'); DarwinInitializationSettings initializationSettingsDarwin = const DarwinInitializationSettings( @@ -274,6 +295,19 @@ class FirebaseMessagingServiceImp implements FirebaseMessagingService { return; } + // Check if user is already on this specific chat + // Try to get ChatVM from Provider, but don't fail if not available + try { + final chatVM = Provider.of(context, listen: false); + if (chatVM.isOnAdChat(adId, receiverUserId)) { + log("✅ User already on ad chat (adId=$adId), skipping navigation"); + return; + } + } catch (e) { + // ChatVM not available in context, proceed with navigation + log("⚠️ Could not check chat context: $e"); + } + log("✅ Navigating to ad chat: adId=$adId"); final chatNavService = injector.get(); await chatNavService.navigateToAdChat( @@ -316,9 +350,9 @@ class FirebaseMessagingServiceImp implements FirebaseMessagingService { String? payload, }) async { const AndroidNotificationDetails androidNotificationDetails = AndroidNotificationDetails( - "0", - 'mowater', - channelDescription: 'mowater_notifications', + 'mowater_chat_channel', // Match the channel ID we created + 'Chat Notifications', + channelDescription: 'Notifications for chat messages', importance: Importance.max, priority: Priority.high, ticker: 'ticker', diff --git a/lib/utils/enums.dart b/lib/utils/enums.dart index b248827..66308aa 100644 --- a/lib/utils/enums.dart +++ b/lib/utils/enums.dart @@ -27,6 +27,8 @@ enum DashboardRouteEnum { fromAdsPayment, fromAdsSubmit, fromReserveAdsPayment, + fromAppointmentPayment, + fromRequestPayment, none, } diff --git a/lib/view_models/ad_view_model.dart b/lib/view_models/ad_view_model.dart index 8bed210..458ba7a 100644 --- a/lib/view_models/ad_view_model.dart +++ b/lib/view_models/ad_view_model.dart @@ -82,6 +82,7 @@ class AdVM extends BaseVM { String damagePartSearchValue = ""; String adReservePriceError = ""; String adPhoneNumberError = ""; + String accidentFreeError = ""; // Edit Variables Amir bool isExtendAdEditEnabled = false; @@ -820,10 +821,12 @@ class AdVM extends BaseVM { notifyListeners(); } - int isInUsed = 0; // 0 = No, 1 = Yes + int? isInUsed; // null = not set, 0 = No, 1 = Yes + String isInUsedError = ""; void updateIsInUsed(int value) { isInUsed = value; + isInUsedError = ""; // Clear error when user makes a selection notifyListeners(); } @@ -833,6 +836,7 @@ class AdVM extends BaseVM { void updateIsAccidentFree(bool value) { isAccidentFree = value; isAccidentFreeSet = true; + accidentFreeError = ""; // Clear error when user makes a selection notifyListeners(); } @@ -1228,6 +1232,23 @@ class AdVM extends BaseVM { } else { vehicleDescError = ""; } + + // Validate Accident Free field (mandatory) + if (!isAccidentFreeSet) { + accidentFreeError = LocaleKeys.pleaseSelectAccidentFree.tr(); + isValidated = false; + } else { + accidentFreeError = ""; + } + + // Validate Is In Used field (mandatory) + if (isInUsed == null) { + isInUsedError = LocaleKeys.pleaseSelectMileageIncrease.tr(); + isValidated = false; + } else { + isInUsedError = ""; + } + if ((pickedPostingImages.isEmpty || pickedPostingImages.length < 3) && !isAdEditEnabled) { vehicleImageError = GlobalConsts.attachImageError; isValidated = false; @@ -1961,9 +1982,11 @@ class AdVM extends BaseVM { adPhoneNumberDialCode = ""; adPhoneNumber = ""; odometer = ""; - isInUsed = 0; + isInUsed = null; + isInUsedError = ""; isAccidentFree = false; isAccidentFreeSet = false; + accidentFreeError = ""; clearSpecialServiceCard(); updateFinanceAvailableStatus(false); notifyListeners(); diff --git a/lib/view_models/appointments_view_model.dart b/lib/view_models/appointments_view_model.dart index 9e846a9..4cff667 100644 --- a/lib/view_models/appointments_view_model.dart +++ b/lib/view_models/appointments_view_model.dart @@ -89,8 +89,7 @@ class AppointmentsVM extends BaseVM { Future onItemsSelectedInService() async { if (currentServiceSelection != null) { - int index = servicesInCurrentAppointment.indexWhere((element) => - element.serviceId == currentServiceSelection!.serviceId!); + int index = servicesInCurrentAppointment.indexWhere((element) => element.serviceId == currentServiceSelection!.serviceId!); if (index == -1) { double totalPrice = 0.0; @@ -108,10 +107,8 @@ class AppointmentsVM extends BaseVM { } Future onPayNowPressedForAppointment({required BuildContext context, required int appointmentID}) async { - context.read().updateAppointmentIdsForPayment( - ids: [appointmentID]); - navigateWithName(context, AppRoutes.paymentMethodsView, - arguments: PaymentTypes.partialAppointment); + context.read().updateAppointmentIdsForPayment(ids: [appointmentID]); + navigateWithName(context, AppRoutes.paymentMethodsView, arguments: PaymentTypes.partialAppointment); } Future onBookAppointmentPressed(BuildContext context) async { @@ -119,8 +116,7 @@ class AppointmentsVM extends BaseVM { bool isSuccess = false; List appointmentIdsList = []; try { - GenericRespModel genericRespModel = await appointmentRepo - .createServiceAppointment( + GenericRespModel genericRespModel = await appointmentRepo.createServiceAppointment( schedules: serviceAppointmentScheduleList, serviceProviderID: selectedBranchModel!.serviceProviderId ?? 0, ); @@ -145,16 +141,13 @@ class AppointmentsVM extends BaseVM { } context.read().onNavbarTapped(1); - applyFilterOnAppointmentsVMForCustomers( - appointmentStatusEnum: AppointmentStatusEnum.booked); + applyFilterOnAppointmentsVMForCustomers(appointmentStatusEnum: AppointmentStatusEnum.booked); Utils.hideLoading(context); resetAfterBookingAppointment(); if (isSuccess) { if (amountToPayForAppointment > 0) { - context.read().updateAppointmentIdsForPayment( - ids: appointmentIdsList); - navigateWithName(context, AppRoutes.paymentMethodsView, - arguments: PaymentTypes.appointment); + context.read().updateAppointmentIdsForPayment(ids: appointmentIdsList); + navigateWithName(context, AppRoutes.paymentMethodsView, arguments: PaymentTypes.appointment); } else { Utils.showToast(LocaleKeys.appointmentBookedSuccessfully.tr()); navigateReplaceWithNameUntilRoute(context, AppRoutes.dashboard); @@ -166,10 +159,8 @@ class AppointmentsVM extends BaseVM { } Future onConfirmAppointmentPressed({required BuildContext context, required appointmentId}) async { - context.read().updateAppointmentIdsForPayment( - ids: [appointmentId]); - navigateWithName(context, AppRoutes.paymentMethodsView, - arguments: PaymentTypes.appointment); + context.read().updateAppointmentIdsForPayment(ids: [appointmentId]); + navigateWithName(context, AppRoutes.paymentMethodsView, arguments: PaymentTypes.appointment); } OfferRequestCommentModel selectedCancelAppointmentReason = OfferRequestCommentModel( @@ -223,24 +214,20 @@ class AppointmentsVM extends BaseVM { Future onCancelAppointmentPressed({required BuildContext context, required AppointmentListModel appointmentListModel}) async { Utils.showLoading(context); try { - GenericRespModel genericRespModel = await appointmentRepo - .cancelOrRescheduleServiceAppointment( + GenericRespModel genericRespModel = await appointmentRepo.cancelOrRescheduleServiceAppointment( serviceAppointmentID: appointmentListModel.id ?? 0, serviceSlotID: appointmentListModel.serviceSlotID ?? 0, appointmentScheduleAction: 2, // 1 for Reschedule and 2 for Cancel ); - if (genericRespModel.messageStatus == 2 || - genericRespModel.data == null) { + if (genericRespModel.messageStatus == 2 || genericRespModel.data == null) { Utils.hideLoading(context); Utils.showToast(genericRespModel.message.toString()); return; } - if (genericRespModel.messageStatus == 1 && - AppState().currentAppType == AppType.customer) { + if (genericRespModel.messageStatus == 1 && AppState().currentAppType == AppType.customer) { context.read().onNavbarTapped(1); - applyFilterOnAppointmentsVMForCustomers( - appointmentStatusEnum: AppointmentStatusEnum.cancelled); + applyFilterOnAppointmentsVMForCustomers(appointmentStatusEnum: AppointmentStatusEnum.cancelled); Utils.showToast(genericRespModel.message.toString()); Utils.hideLoading(context); navigateReplaceWithNameUntilRoute(context, AppRoutes.dashboard); @@ -259,8 +246,7 @@ class AppointmentsVM extends BaseVM { notifyListeners(); } - SelectionModel branchSelectedCategoryId = SelectionModel( - selectedOption: "", selectedId: -1, errorValue: ""); + SelectionModel branchSelectedCategoryId = SelectionModel(selectedOption: "", selectedId: -1, errorValue: ""); void updateProviderCategoryId(SelectionModel id) { branchSelectedCategoryId = id; @@ -269,6 +255,7 @@ class AppointmentsVM extends BaseVM { } List branchesCategoriesFilterOptions = []; + ScrollController branchesFilterScrollController = ScrollController(); List servicesInCurrentAppointment = []; ServiceModel? currentServiceSelection; @@ -288,8 +275,7 @@ class AppointmentsVM extends BaseVM { service.servicelocationInfo.latitude = value.latitude; service.servicelocationInfo.longitude = value.longitude; service.servicelocationInfo.distanceToBranch = value.distanceToBranch; - service.servicelocationInfo.homeChargesInCurrentService = - double.parse(service.rangePricePerKm ?? "0.0"); + service.servicelocationInfo.homeChargesInCurrentService = double.parse(service.rangePricePerKm ?? "0.0"); } } @@ -298,9 +284,7 @@ class AppointmentsVM extends BaseVM { void updateBranchServiceId(SelectionModel id) async { branchSelectedServiceId = id; - currentServiceSelection = branchServices.firstWhere((element) => - element - .serviceProviderServiceId == id.selectedId); + currentServiceSelection = branchServices.firstWhere((element) => element.serviceProviderServiceId == id.selectedId); if (currentServiceSelection == null) { return; } @@ -310,15 +294,13 @@ class AppointmentsVM extends BaseVM { return; } if (currentLocationInfoModel != null) { - currentLocationInfoModel!.homeChargesInCurrentService = - double.parse(currentServiceSelection!.rangePricePerKm ?? "0.0"); + currentLocationInfoModel!.homeChargesInCurrentService = double.parse(currentServiceSelection!.rangePricePerKm ?? "0.0"); currentServiceSelection!.servicelocationInfo = CurrentLocationInfoModel( address: currentLocationInfoModel!.address, latitude: currentLocationInfoModel!.latitude, longitude: currentLocationInfoModel!.longitude, distanceToBranch: currentLocationInfoModel!.distanceToBranch, - homeChargesInCurrentService: double.parse( - currentServiceSelection!.rangePricePerKm ?? "0.0"), + homeChargesInCurrentService: double.parse(currentServiceSelection!.rangePricePerKm ?? "0.0"), ); } @@ -326,11 +308,8 @@ class AppointmentsVM extends BaseVM { } void removeServiceInCurrentAppointment(int index) { - int serviceId = servicesInCurrentAppointment - .elementAt(index) - .serviceProviderServiceId ?? -1; - allSelectedItemsInAppointments.removeWhere((element) => - element.serviceProviderServiceId == serviceId); + int serviceId = servicesInCurrentAppointment.elementAt(index).serviceProviderServiceId ?? -1; + allSelectedItemsInAppointments.removeWhere((element) => element.serviceProviderServiceId == serviceId); servicesInCurrentAppointment[index].serviceItems!.clear(); servicesInCurrentAppointment.removeAt(index); notifyListeners(); @@ -338,11 +317,9 @@ class AppointmentsVM extends BaseVM { resetCategorySelectionBottomSheet() { selectedSubServicesCounter = 0; - branchSelectedCategoryId = - SelectionModel(selectedOption: "", selectedId: -1, errorValue: ""); + branchSelectedCategoryId = SelectionModel(selectedOption: "", selectedId: -1, errorValue: ""); isHomeTapped = false; - branchSelectedServiceId = - SelectionModel(selectedOption: "", selectedId: -1, errorValue: ""); + branchSelectedServiceId = SelectionModel(selectedOption: "", selectedId: -1, errorValue: ""); currentServiceSelection = null; } @@ -358,8 +335,7 @@ class AppointmentsVM extends BaseVM { servicesInCurrentAppointment.clear(); servicesInCurrentAppointment = []; currentLocationInfoModel = null; - log("here servicesInCurrentAppointment: ${servicesInCurrentAppointment - .length}"); + log("here servicesInCurrentAppointment: ${servicesInCurrentAppointment.length}"); } List myAppointmentsEnum = []; @@ -367,18 +343,12 @@ class AppointmentsVM extends BaseVM { Future populateAppointmentsFilterList() async { if (appointmentsFilterOptions.isNotEmpty) return; - myAppointmentsEnum = await commonRepo.getEnumTypeValues(enumTypeID: AppEnums - .appointmentsFilterEnumId); // 13 is to get Appointments Filter Enums + myAppointmentsEnum = await commonRepo.getEnumTypeValues(enumTypeID: AppEnums.appointmentsFilterEnumId); // 13 is to get Appointments Filter Enums for (int i = 0; i < myAppointmentsEnum.length; i++) { - appointmentsFilterOptions.add(FilterListModel( - title: myAppointmentsEnum[i].enumValueStr, - titleN: myAppointmentsEnum[i].enumValueStrN, - isSelected: false, - id: myAppointmentsEnum[i].enumValue)); + appointmentsFilterOptions.add(FilterListModel(title: myAppointmentsEnum[i].enumValueStr, titleN: myAppointmentsEnum[i].enumValueStrN, isSelected: false, id: myAppointmentsEnum[i].enumValue)); } - appointmentsFilterOptions.insert( - 0, FilterListModel(title: "All Appointments", titleN: "جميع الفروع", isSelected: true, id: 0)); + appointmentsFilterOptions.insert(0, FilterListModel(title: "All Appointments", titleN: "جميع الفروع", isSelected: true, id: 0)); notifyListeners(); } @@ -390,36 +360,27 @@ class AppointmentsVM extends BaseVM { for (var value in appointmentsFilterOptions) { value.isSelected = false; } - int index = appointmentsFilterOptions.indexWhere((element) => - element.id == appointmentStatusEnum.getIdFromAppointmentStatusEnum()); + int index = appointmentsFilterOptions.indexWhere((element) => element.id == appointmentStatusEnum.getIdFromAppointmentStatusEnum()); appointmentsFilterOptions[index].isSelected = true; isAppointmentLoading = true; notifyListeners(); - myFilteredAppointmentsForProvider = - await getAppointmentsBasedOnFiltersForProviders( - branchID: branchID ?? selectedBranchIdForAppointments, - appointmentStatus: appointmentStatusEnum); + myFilteredAppointmentsForProvider = await getAppointmentsBasedOnFiltersForProviders(branchID: branchID ?? selectedBranchIdForAppointments, appointmentStatus: appointmentStatusEnum); isAppointmentLoading = false; if (shouldPopulateUpcoming) { myUpComingAppointments = myFilteredAppointmentsForProvider .where((element) => - (element.appointmentStatusEnum == AppointmentStatusEnum.booked || - element.appointmentStatusEnum == AppointmentStatusEnum.confirmed) && - (DateHelper.parseStringToDate(element.appointmentDate!).isAfter( - DateTime.now()))) + (element.appointmentStatusEnum == AppointmentStatusEnum.booked || element.appointmentStatusEnum == AppointmentStatusEnum.confirmed) && + (DateHelper.parseStringToDate(element.appointmentDate!).isAfter(DateTime.now()))) .toList(); log("myUpComingAppointments: ${myUpComingAppointments.length}"); } if (isNeedCustomerFilter) { - log( - "myFilteredAppointmentsForProvider: ${myFilteredAppointmentsForProvider - .length}"); + log("myFilteredAppointmentsForProvider: ${myFilteredAppointmentsForProvider.length}"); - myFilteredAppointmentsForProvider = - findAppointmentsBasedOnCustomers(myFilteredAppointmentsForProvider); + myFilteredAppointmentsForProvider = findAppointmentsBasedOnCustomers(myFilteredAppointmentsForProvider); } notifyListeners(); } @@ -430,23 +391,18 @@ class AppointmentsVM extends BaseVM { for (var value in appointmentsFilterOptions) { value.isSelected = false; } - int index = appointmentsFilterOptions.indexWhere((element) => - element.id == appointmentStatusEnum.getIdFromAppointmentStatusEnum()); + int index = appointmentsFilterOptions.indexWhere((element) => element.id == appointmentStatusEnum.getIdFromAppointmentStatusEnum()); appointmentsFilterOptions[index].isSelected = true; isAppointmentLoading = true; notifyListeners(); - myFilteredAppointmentsForCustomers = - await getAppointmentsBasedOnFiltersForCustomer( - appointmentStatusEnum: appointmentStatusEnum); + myFilteredAppointmentsForCustomers = await getAppointmentsBasedOnFiltersForCustomer(appointmentStatusEnum: appointmentStatusEnum); isAppointmentLoading = false; if (shouldPopulateUpcoming) { myUpComingAppointments = myFilteredAppointmentsForCustomers .where((element) => - (element.appointmentStatusEnum == AppointmentStatusEnum.booked || - element.appointmentStatusEnum == AppointmentStatusEnum.confirmed) && - (DateHelper.parseStringToDate(element.appointmentDate!).isAfter( - DateTime.now()))) + (element.appointmentStatusEnum == AppointmentStatusEnum.booked || element.appointmentStatusEnum == AppointmentStatusEnum.confirmed) && + (DateHelper.parseStringToDate(element.appointmentDate!).isAfter(DateTime.now()))) .toList(); } @@ -460,15 +416,10 @@ class AppointmentsVM extends BaseVM { } List groupedList = uniqueCustomerIDs.map((id) { - List list = appointments.where((item) => - item.customerID == id).toList(); - list.sort((a, b) => - DateHelper - .parseStringToDate(DateHelper.formatDateT(b.appointmentDate!)) - .millisecondsSinceEpoch - .compareTo(DateHelper - .parseStringToDate(DateHelper.formatDateT(a.appointmentDate!)) - .millisecondsSinceEpoch)); + List list = appointments.where((item) => item.customerID == id).toList(); + list.sort((a, b) => DateHelper.parseStringToDate(DateHelper.formatDateT(b.appointmentDate!)) + .millisecondsSinceEpoch + .compareTo(DateHelper.parseStringToDate(DateHelper.formatDateT(a.appointmentDate!)).millisecondsSinceEpoch)); AppointmentListModel model = list.first; model.customerAppointmentList = list; return model; @@ -485,10 +436,7 @@ class AppointmentsVM extends BaseVM { isSlotsLoading = true; notifyListeners(); try { - GenericRespModel genericRespModel = await appointmentRepo - .getAppointmentSlots(branchID: branchID, - fromDate: selectedDateForAppointments, - toDate: selectedDateForAppointments); + GenericRespModel genericRespModel = await appointmentRepo.getAppointmentSlots(branchID: branchID, fromDate: selectedDateForAppointments, toDate: selectedDateForAppointments); if (genericRespModel.messageStatus == 1) { appointmentSlots = AppointmentSlots.fromJson(genericRespModel.data); } else { @@ -506,9 +454,7 @@ class AppointmentsVM extends BaseVM { Future updateAppointmentStatus({required int appointmentId, required AppointmentStatusEnum appointmentStatusEnum, bool isNeedToRebuild = false}) async { if (isNeedToRebuild) setState(ViewState.busy); try { - GenericRespModel genericRespModel = await appointmentRepo - .updateAppointmentStatus(appointmentId: appointmentId, - appointmentStatusEnum: appointmentStatusEnum); + GenericRespModel genericRespModel = await appointmentRepo.updateAppointmentStatus(appointmentId: appointmentId, appointmentStatusEnum: appointmentStatusEnum); if (genericRespModel.messageStatus == 1) { Utils.showToast(LocaleKeys.appointmentStatusUpdated.tr()); @@ -523,12 +469,10 @@ class AppointmentsVM extends BaseVM { } } - Future updateAppointmentPaymentStatus(Map map, - {bool isNeedToRebuild = false}) async { + Future updateAppointmentPaymentStatus(Map map, {bool isNeedToRebuild = false}) async { if (isNeedToRebuild) setState(ViewState.busy); try { - GenericRespModel genericRespModel = await appointmentRepo - .updateAppointmentPaymentStatus(map); + GenericRespModel genericRespModel = await appointmentRepo.updateAppointmentPaymentStatus(map); if (genericRespModel.messageStatus == 1) { Utils.showToast(LocaleKeys.paymentStatusUpdated.tr()); @@ -544,22 +488,16 @@ class AppointmentsVM extends BaseVM { } bool isShowMergeButton() { - if (myFilteredAppointmentsForProvider[selectedAppointmentIndex] - .customerAppointmentList == null) { + if (myFilteredAppointmentsForProvider[selectedAppointmentIndex].customerAppointmentList == null) { return false; } - return myFilteredAppointmentsForProvider[selectedAppointmentIndex] - .customerAppointmentList!.every((appointment) => - appointment.appointmentStatusEnum == AppointmentStatusEnum.confirmed) && - myFilteredAppointmentsForProvider[selectedAppointmentIndex] - .customerAppointmentList!.length > 1; + return myFilteredAppointmentsForProvider[selectedAppointmentIndex].customerAppointmentList!.every((appointment) => appointment.appointmentStatusEnum == AppointmentStatusEnum.confirmed) && + myFilteredAppointmentsForProvider[selectedAppointmentIndex].customerAppointmentList!.length > 1; } - Future createMergeAppointment(Map map, - {bool isNeedToRebuild = false}) async { + Future createMergeAppointment(Map map, {bool isNeedToRebuild = false}) async { if (isNeedToRebuild) setState(ViewState.busy); - GenericRespModel genericRespModel = await appointmentRepo - .createMergeAppointment(map); + GenericRespModel genericRespModel = await appointmentRepo.createMergeAppointment(map); return genericRespModel; } @@ -567,14 +505,10 @@ class AppointmentsVM extends BaseVM { bool inNeedToEnableMergeButton = false; void updateCheckBoxInMergeRequest(int currentIndex) { - myFilteredAppointmentsForProvider[selectedAppointmentIndex] - .customerAppointmentList![currentIndex].isSelected = - !(myFilteredAppointmentsForProvider[selectedAppointmentIndex] - .customerAppointmentList?[currentIndex].isSelected ?? false); - - int count = countSelected( - myFilteredAppointmentsForProvider[selectedAppointmentIndex] - .customerAppointmentList ?? []); + myFilteredAppointmentsForProvider[selectedAppointmentIndex].customerAppointmentList![currentIndex].isSelected = + !(myFilteredAppointmentsForProvider[selectedAppointmentIndex].customerAppointmentList?[currentIndex].isSelected ?? false); + + int count = countSelected(myFilteredAppointmentsForProvider[selectedAppointmentIndex].customerAppointmentList ?? []); if (count > 1) { inNeedToEnableMergeButton = true; } else { @@ -584,50 +518,35 @@ class AppointmentsVM extends BaseVM { } int countSelected(List appointments) { - return appointments - .where((appointment) => appointment.isSelected == true) - .toList() - .length; + return appointments.where((appointment) => appointment.isSelected == true).toList().length; } updateSelectedAppointmentDate({required int dateIndex, required int scheduleIndex}) { - for (var element in serviceAppointmentScheduleList[scheduleIndex] - .customTimeDateSlotList!) { + for (var element in serviceAppointmentScheduleList[scheduleIndex].customTimeDateSlotList!) { element.date!.isSelected = false; } - serviceAppointmentScheduleList[scheduleIndex] - .customTimeDateSlotList![dateIndex].date!.isSelected = true; + serviceAppointmentScheduleList[scheduleIndex].customTimeDateSlotList![dateIndex].date!.isSelected = true; serviceAppointmentScheduleList[scheduleIndex].selectedDateIndex = dateIndex; final date = TimeSlotModel( - date: serviceAppointmentScheduleList[scheduleIndex] - .customTimeDateSlotList![dateIndex].date!.date, - slotId: serviceAppointmentScheduleList[scheduleIndex] - .customTimeDateSlotList![dateIndex].date!.slotId, + date: serviceAppointmentScheduleList[scheduleIndex].customTimeDateSlotList![dateIndex].date!.date, + slotId: serviceAppointmentScheduleList[scheduleIndex].customTimeDateSlotList![dateIndex].date!.slotId, isSelected: true, slot: "", ); - serviceAppointmentScheduleList[scheduleIndex] - .selectedCustomTimeDateSlotModel = CustomTimeDateSlotModel(date: date); + serviceAppointmentScheduleList[scheduleIndex].selectedCustomTimeDateSlotModel = CustomTimeDateSlotModel(date: date); notifyListeners(); } updateSelectedAppointmentSlotByDate({required int scheduleIndex, required int slotIndex}) { - for (var element in serviceAppointmentScheduleList[scheduleIndex] - .customTimeDateSlotList!) { + for (var element in serviceAppointmentScheduleList[scheduleIndex].customTimeDateSlotList!) { for (var element in element.availableSlots!) { element.isSelected = false; } } - int dateIndex = serviceAppointmentScheduleList[scheduleIndex] - .selectedDateIndex!; - serviceAppointmentScheduleList[scheduleIndex] - .customTimeDateSlotList![dateIndex].availableSlots![slotIndex] - .isSelected = true; - serviceAppointmentScheduleList[scheduleIndex] - .selectedCustomTimeDateSlotModel!.availableSlots = - serviceAppointmentScheduleList[scheduleIndex] - .customTimeDateSlotList![dateIndex].availableSlots!; + int dateIndex = serviceAppointmentScheduleList[scheduleIndex].selectedDateIndex!; + serviceAppointmentScheduleList[scheduleIndex].customTimeDateSlotList![dateIndex].availableSlots![slotIndex].isSelected = true; + serviceAppointmentScheduleList[scheduleIndex].selectedCustomTimeDateSlotModel!.availableSlots = serviceAppointmentScheduleList[scheduleIndex].customTimeDateSlotList![dateIndex].availableSlots!; notifyListeners(); } @@ -658,27 +577,18 @@ class AppointmentsVM extends BaseVM { if (serviceIndex != -1) { // servicesInCurrentAppointment[serviceIndex].serviceItems!.add(serviceItemsFromApi[index]); servicesInCurrentAppointment[serviceIndex].currentTotalServicePrice = - servicesInCurrentAppointment[serviceIndex] - .currentTotalServicePrice + - double.parse((serviceItemsFromApi[index].price) ?? "0.0"); + servicesInCurrentAppointment[serviceIndex].currentTotalServicePrice + double.parse((serviceItemsFromApi[index].price) ?? "0.0"); } } } else { selectedSubServicesCounter = selectedSubServicesCounter - 1; - currentServiceSelection!.serviceItems!.removeWhere((element) => - element.id == itemId); - allSelectedItemsInAppointments.removeWhere((element) => - element.id == itemId); + currentServiceSelection!.serviceItems!.removeWhere((element) => element.id == itemId); + allSelectedItemsInAppointments.removeWhere((element) => element.id == itemId); if (serviceIndex != -1) { servicesInCurrentAppointment[serviceIndex].currentTotalServicePrice = - servicesInCurrentAppointment[serviceIndex] - .currentTotalServicePrice - - double.parse((serviceItemsFromApi[index].price) ?? "0.0"); - if (servicesInCurrentAppointment[serviceIndex] - .currentTotalServicePrice < 0.0) { - servicesInCurrentAppointment[serviceIndex].currentTotalServicePrice = - servicesInCurrentAppointment[serviceIndex] - .currentTotalServicePrice * -1; + servicesInCurrentAppointment[serviceIndex].currentTotalServicePrice - double.parse((serviceItemsFromApi[index].price) ?? "0.0"); + if (servicesInCurrentAppointment[serviceIndex].currentTotalServicePrice < 0.0) { + servicesInCurrentAppointment[serviceIndex].currentTotalServicePrice = servicesInCurrentAppointment[serviceIndex].currentTotalServicePrice * -1; } // servicesInCurrentAppointment[serviceIndex].serviceItems!.removeWhere((element) => element.id == itemId); } @@ -694,16 +604,29 @@ class AppointmentsVM extends BaseVM { } branchesCategoriesFilterOptions[index].isSelected = true; - await getBranchesBasedOnCategoryFilters( - serviceId: branchesCategoriesFilterOptions[index].id); + await getBranchesBasedOnCategoryFilters(serviceId: branchesCategoriesFilterOptions[index].id); + + // Auto-scroll to the selected filter + if (branchesFilterScrollController.hasClients) { + // Calculate the scroll offset to center the selected item + // Each filter chip is approximately 88 pixels wide (80 minWidth + 8 padding + margins) + final double itemWidth = 88.0; + final double targetOffset = (index * itemWidth) - 50; // -50 to add some left padding + + branchesFilterScrollController.animateTo( + targetOffset.clamp(0.0, branchesFilterScrollController.position.maxScrollExtent), + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + } + notifyListeners(); } Future> getServiceItems({required int serviceId}) async { setState(ViewState.busy); serviceItemsFromApi.clear(); - serviceItemsFromApi = - await branchRepo.getServiceItems(serviceId: serviceId); + serviceItemsFromApi = await branchRepo.getServiceItems(serviceId: serviceId); selectedSubServicesCounter = 0; for (var item in serviceItemsFromApi) { if (ifItemAlreadySelected(item.id!)) { @@ -718,8 +641,7 @@ class AppointmentsVM extends BaseVM { String pickHomeLocationError = ""; String selectSubServicesError = ""; - SelectionModel branchSelectedServiceId = SelectionModel( - selectedOption: "", selectedId: -1, errorValue: ""); + SelectionModel branchSelectedServiceId = SelectionModel(selectedOption: "", selectedId: -1, errorValue: ""); void updatePickHomeLocationError(String value) { pickHomeLocationError = value; @@ -732,8 +654,7 @@ class AppointmentsVM extends BaseVM { } if (isHomeTapped) { - if (currentLocationInfoModel == null || - currentLocationInfoModel!.address == "") { + if (currentLocationInfoModel == null || currentLocationInfoModel!.address == "") { updatePickHomeLocationError(GlobalConsts.homeLocationEmptyError); return false; } @@ -760,8 +681,7 @@ class AppointmentsVM extends BaseVM { return totalPrice.toString(); } - void openTheAddServiceBottomSheet(BuildContext context, - AppointmentsVM appointmentsVM) { + void openTheAddServiceBottomSheet(BuildContext context, AppointmentsVM appointmentsVM) { showModalBottomSheet( context: context, isScrollControlled: true, @@ -772,22 +692,16 @@ class AppointmentsVM extends BaseVM { ); } - void priceBreakDownClicked(BuildContext context, - ServiceModel selectedService) { + void priceBreakDownClicked(BuildContext context, ServiceModel selectedService) { showModalBottomSheet( context: context, isScrollControlled: true, enableDrag: true, builder: (BuildContext context) { - double totalKms = double.parse( - selectedService.servicelocationInfo.distanceToBranch - .toStringAsFixed(2)); - double pricePerKm = double.parse( - selectedService.servicelocationInfo.homeChargesInCurrentService - .toStringAsFixed(2)); + double totalKms = double.parse(selectedService.servicelocationInfo.distanceToBranch.toStringAsFixed(2)); + double pricePerKm = double.parse(selectedService.servicelocationInfo.homeChargesInCurrentService.toStringAsFixed(2)); return InfoBottomSheet( - title: LocaleKeys.chargesBreakdown.tr().toText( - fontSize: 24, isBold: true), + title: LocaleKeys.chargesBreakdown.tr().toText(fontSize: 24, isBold: true), description: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -795,49 +709,36 @@ class AppointmentsVM extends BaseVM { Column( children: List.generate( selectedService.serviceItems!.length, - (index) => - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - "${selectedService.serviceItems![index].name}" - .toText(fontSize: 12, - color: MyColors.lightTextColor, - isBold: true), - "${selectedService.serviceItems![index] - .price} ${LocaleKeys.sar.tr()}".toText( - fontSize: 12, isBold: true), - ], - ), + (index) => Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + "${selectedService.serviceItems![index].name}".toText(fontSize: 12, color: MyColors.lightTextColor, isBold: true), + "${selectedService.serviceItems![index].price} ${LocaleKeys.sar.tr()}".toText(fontSize: 12, isBold: true), + ], + ), ), ), Row( mainAxisAlignment: MainAxisAlignment.end, children: [ - "${selectedService.currentTotalServicePrice} ${LocaleKeys - .sar.tr()}".toText(fontSize: 16, isBold: true), + "${selectedService.currentTotalServicePrice} ${LocaleKeys.sar.tr()}".toText(fontSize: 16, isBold: true), ], ), if (selectedService.isHomeSelected) ...[ 20.height, - LocaleKeys.homeLocation.tr().toText( - fontSize: 16, isBold: true), + LocaleKeys.homeLocation.tr().toText(fontSize: 16, isBold: true), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - "${totalKms}km ".toText(fontSize: 12, - color: MyColors.lightTextColor, - isBold: true), - "$pricePerKm x $totalKms".toText( - fontSize: 12, isBold: true), + "${totalKms}km ".toText(fontSize: 12, color: MyColors.lightTextColor, isBold: true), + "$pricePerKm x $totalKms".toText(fontSize: 12, isBold: true), ], ), 8.height, Row( mainAxisAlignment: MainAxisAlignment.end, children: [ - "${((pricePerKm) * totalKms).toStringAsFixed( - 2)} ${LocaleKeys.sar.tr()}".toText( - fontSize: 16, isBold: true), + "${((pricePerKm) * totalKms).toStringAsFixed(2)} ${LocaleKeys.sar.tr()}".toText(fontSize: 16, isBold: true), ], ), ], @@ -845,21 +746,14 @@ class AppointmentsVM extends BaseVM { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - LocaleKeys.totalAmount.tr().toText( - fontSize: 16, isBold: true), + LocaleKeys.totalAmount.tr().toText(fontSize: 16, isBold: true), Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ - (selectedService.isHomeSelected - ? ((selectedService.currentTotalServicePrice) + - (pricePerKm * totalKms)).toStringAsFixed(2) - : "${selectedService.currentTotalServicePrice}") + (selectedService.isHomeSelected ? ((selectedService.currentTotalServicePrice) + (pricePerKm * totalKms)).toStringAsFixed(2) : "${selectedService.currentTotalServicePrice}") .toText(fontSize: 29, isBold: true), 2.width, - LocaleKeys.sar.tr().toText( - color: MyColors.lightTextColor, - fontSize: 16, - isBold: true).paddingOnly(bottom: 5), + LocaleKeys.sar.tr().toText(color: MyColors.lightTextColor, fontSize: 16, isBold: true).paddingOnly(bottom: 5), ], ) ], @@ -879,8 +773,7 @@ class AppointmentsVM extends BaseVM { isValidated = false; break; } - if (schedule.selectedCustomTimeDateSlotModel!.date == null || - !schedule.selectedCustomTimeDateSlotModel!.date!.isSelected) { + if (schedule.selectedCustomTimeDateSlotModel!.date == null || !schedule.selectedCustomTimeDateSlotModel!.date!.isSelected) { isValidated = false; break; } else { @@ -888,8 +781,7 @@ class AppointmentsVM extends BaseVM { isValidated = false; break; } else { - TimeSlotModel slot = schedule.selectedCustomTimeDateSlotModel! - .availableSlots!.firstWhere((element) => element.isSelected); + TimeSlotModel slot = schedule.selectedCustomTimeDateSlotModel!.availableSlots!.firstWhere((element) => element.isSelected); if (slot.date.isNotEmpty) { isValidated = true; break; @@ -921,8 +813,7 @@ class AppointmentsVM extends BaseVM { } } - serviceAppointmentScheduleList = - await appointmentRepo.mergeServiceIntoAvailableSchedules( + serviceAppointmentScheduleList = await appointmentRepo.mergeServiceIntoAvailableSchedules( serviceItemIdsForHome: serviceItemIdsForHome, serviceItemIdsForWorkshop: serviceItemIdsForWorkshop, ); @@ -935,38 +826,25 @@ class AppointmentsVM extends BaseVM { totalAmount = 0.0; amountToPayForAppointment = 0.0; for (var schedule in serviceAppointmentScheduleList) { - amountToPayForAppointment = - amountToPayForAppointment + (schedule.amountToPay ?? 0.0); + amountToPayForAppointment = amountToPayForAppointment + (schedule.amountToPay ?? 0.0); totalAmount = totalAmount + (schedule.amountTotal ?? 0.0); for (var service in schedule.servicesListInAppointment!) { - int index = servicesInCurrentAppointment.indexWhere((element) => - element.serviceProviderServiceId == service.serviceProviderServiceId); + int index = servicesInCurrentAppointment.indexWhere((element) => element.serviceProviderServiceId == service.serviceProviderServiceId); if (index != -1) { if (servicesInCurrentAppointment[index].isHomeSelected) { - service.servicelocationInfo = - servicesInCurrentAppointment[index].servicelocationInfo; - schedule.locationInfoModel = - servicesInCurrentAppointment[index].servicelocationInfo; + service.servicelocationInfo = servicesInCurrentAppointment[index].servicelocationInfo; + schedule.locationInfoModel = servicesInCurrentAppointment[index].servicelocationInfo; totalAmount = - totalAmount + - ((servicesInCurrentAppointment[index].servicelocationInfo - .distanceToBranch) * - servicesInCurrentAppointment[index].servicelocationInfo - .homeChargesInCurrentService); + totalAmount + ((servicesInCurrentAppointment[index].servicelocationInfo.distanceToBranch) * servicesInCurrentAppointment[index].servicelocationInfo.homeChargesInCurrentService); schedule.totalLocationCharges = schedule.totalLocationCharges! + - (servicesInCurrentAppointment[index].servicelocationInfo - .distanceToBranch * - servicesInCurrentAppointment[index].servicelocationInfo - .homeChargesInCurrentService); + (servicesInCurrentAppointment[index].servicelocationInfo.distanceToBranch * servicesInCurrentAppointment[index].servicelocationInfo.homeChargesInCurrentService); } } } } Utils.hideLoading(context); - navigateWithName(context, AppRoutes.bookAppointmenSchedulesView, - arguments: ScreenArgumentsForAppointmentDetailPage( - routeFlag: 1, appointmentId: 0)); // 1 For Creating an Appointment + navigateWithName(context, AppRoutes.bookAppointmenSchedulesView, arguments: ScreenArgumentsForAppointmentDetailPage(routeFlag: 1, appointmentId: 0)); // 1 For Creating an Appointment notifyListeners(); } @@ -988,8 +866,7 @@ class AppointmentsVM extends BaseVM { } } - serviceAppointmentScheduleList = - await appointmentRepo.mergeServiceIntoAvailableSchedules( + serviceAppointmentScheduleList = await appointmentRepo.mergeServiceIntoAvailableSchedules( serviceItemIdsForHome: serviceItemIdsForHome, serviceItemIdsForWorkshop: serviceItemIdsForWorkshop, ); @@ -1004,8 +881,7 @@ class AppointmentsVM extends BaseVM { navigateWithName( context, AppRoutes.bookAppointmenSchedulesView, - arguments: ScreenArgumentsForAppointmentDetailPage( - routeFlag: 2, appointmentId: appointmentListModel.id ?? 0), + arguments: ScreenArgumentsForAppointmentDetailPage(routeFlag: 2, appointmentId: appointmentListModel.id ?? 0), ); // 2 For Rescheduling an Appointment notifyListeners(); @@ -1014,15 +890,13 @@ class AppointmentsVM extends BaseVM { Future onRescheduleAppointmentConfirmPressed({required BuildContext context, required int appointmentId, required int selectedSlotId}) async { Utils.showLoading(context); try { - GenericRespModel genericRespModel = await appointmentRepo - .cancelOrRescheduleServiceAppointment( + GenericRespModel genericRespModel = await appointmentRepo.cancelOrRescheduleServiceAppointment( serviceAppointmentID: appointmentId, serviceSlotID: selectedSlotId, appointmentScheduleAction: 1, // 1 for Reschedule and 2 for Cancel ); - if (genericRespModel.messageStatus == 2 || - genericRespModel.data == null) { + if (genericRespModel.messageStatus == 2 || genericRespModel.data == null) { Utils.hideLoading(context); Utils.showToast(genericRespModel.message.toString()); return; @@ -1030,8 +904,7 @@ class AppointmentsVM extends BaseVM { if (genericRespModel.messageStatus == 1) { context.read().onNavbarTapped(1); Utils.showToast(genericRespModel.message.toString()); - applyFilterOnAppointmentsVMForCustomers( - appointmentStatusEnum: AppointmentStatusEnum.cancelled); + applyFilterOnAppointmentsVMForCustomers(appointmentStatusEnum: AppointmentStatusEnum.cancelled); Utils.hideLoading(context); navigateReplaceWithNameUntilRoute(context, AppRoutes.dashboard); } @@ -1059,19 +932,14 @@ class AppointmentsVM extends BaseVM { enableDrag: true, builder: (BuildContext context) { return InfoBottomSheet( - title: LocaleKeys.reportComplaint.tr().toText( - fontSize: 28, isBold: true, letterSpacing: -1.44), + title: LocaleKeys.reportComplaint.tr().toText(fontSize: 28, isBold: true, letterSpacing: -1.44), description: Padding( - padding: EdgeInsets.only(bottom: MediaQuery - .of(context) - .viewInsets - .bottom), + padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (customerName.isNotEmpty) ...[ - ("${LocaleKeys.customerName.tr()}: $customerName").toText( - fontSize: 15, color: MyColors.lightTextColor), + ("${LocaleKeys.customerName.tr()}: $customerName").toText(fontSize: 15, color: MyColors.lightTextColor), ], 10.height, TxtField( @@ -1117,10 +985,7 @@ class AppointmentsVM extends BaseVM { Future reportComplaintAgainstCustomer({required int customerID, required String comment, required int complaintType, required BuildContext context}) async { Utils.showLoading(context); try { - GenericRespModel genericRespModel = await commonRepo - .createComplainFromProvider(customerId: customerID, - comment: comment, - complainType: complaintType); + GenericRespModel genericRespModel = await commonRepo.createComplainFromProvider(customerId: customerID, comment: comment, complainType: complaintType); Utils.showToast(genericRespModel.message.toString()); Utils.hideLoading(context); return genericRespModel.messageStatus == 1; @@ -1135,8 +1000,7 @@ class AppointmentsVM extends BaseVM { Future getProvidersContactInfo({required int providerId, required BuildContext context}) async { Utils.showLoading(context); try { - ProviderContactInfoModel contactInfoModel = await commonRepo - .getProvidersContactInfo(providerId: providerId); + ProviderContactInfoModel contactInfoModel = await commonRepo.getProvidersContactInfo(providerId: providerId); Utils.hideLoading(context); return contactInfoModel; } catch (e, s) { @@ -1159,18 +1023,17 @@ class AppointmentsVM extends BaseVM { if (branchesCategoriesFilterOptions.isNotEmpty) return; branchesCategoriesFilterOptions.clear(); setOnlyState(ViewState.busy); - Services services = await branchRepo.fetchServicesByCategoryId( - serviceCategoryId: -1); + Services services = await branchRepo.fetchServicesByCategoryId(serviceCategoryId: -1); services.data?.forEach((element) { branchesCategoriesFilterOptions.add(FilterListModel( id: element.id ?? 0, isSelected: false, iconUrl: element.serviceIconUrl ?? "", title: element.description ?? "", - titleN: element.descriptionN ?? "",)); + titleN: element.descriptionN ?? "", + )); }); - branchesCategoriesFilterOptions.insert( - 0, FilterListModel(id: 0, isSelected: true, title: "All Branches", titleN: "جميع الفروع")); + branchesCategoriesFilterOptions.insert(0, FilterListModel(id: 0, isSelected: true, title: "All Branches", titleN: "جميع الفروع")); notifyListeners(); setState(ViewState.idle); } @@ -1215,16 +1078,14 @@ class AppointmentsVM extends BaseVM { void getBranchCategories() async { for (var value in selectedBranchModel!.branchServices!) { if (!isCategoryAlreadyPresent(value.categoryId!)) { - branchCategories.add( - DropValue(value.categoryId!, value.categoryName!, "")); + branchCategories.add(DropValue(value.categoryId!, value.categoryName!, "")); } } notifyListeners(); } Future getBranchServices({required int categoryId}) async { - branchSelectedServiceId = - SelectionModel(selectedOption: "", selectedId: -1, errorValue: ""); + branchSelectedServiceId = SelectionModel(selectedOption: "", selectedId: -1, errorValue: ""); isHomeTapped = false; pickHomeLocationError = ""; if (categoryId != -1) { @@ -1236,8 +1097,7 @@ class AppointmentsVM extends BaseVM { } List getFilteredBranchServices({required int categoryId}) { - List filteredServices = selectedBranchModel!.branchServices! - .where((element) => element.categoryId == categoryId).toList(); + List filteredServices = selectedBranchModel!.branchServices!.where((element) => element.categoryId == categoryId).toList(); return filteredServices; } @@ -1246,8 +1106,7 @@ class AppointmentsVM extends BaseVM { double longitude = AppState().currentLocation.longitude; setState(ViewState.busy); try { - providerProfileModel = await branchRepo.getBranchAndServicesByProviderId( - providerId, latitude: latitude, longitude: longitude); + providerProfileModel = await branchRepo.getBranchAndServicesByProviderId(providerId, latitude: latitude, longitude: longitude); } catch (e, s) { logger.e(s); setState(ViewState.idle); @@ -1309,16 +1168,12 @@ class AppointmentsVM extends BaseVM { notifyListeners(); } - SelectionModel branchFilterSelectedProviderId = SelectionModel( - selectedOption: "", selectedId: -1, errorValue: ""); + SelectionModel branchFilterSelectedProviderId = SelectionModel(selectedOption: "", selectedId: -1, errorValue: ""); - void updateBranchFilterSelectedProviderId(SelectionModel id, - {bool isForSearch = false}) async { + void updateBranchFilterSelectedProviderId(SelectionModel id, {bool isForSearch = false}) async { if (isForSearch) { - DropValue providerDrop = providersDropList.firstWhere((element) => - element.id == id.selectedId); - if (!ifAlreadyExist( - list: branchFilterProviderSearchHistory, value: providerDrop)) { + DropValue providerDrop = providersDropList.firstWhere((element) => element.id == id.selectedId); + if (!ifAlreadyExist(list: branchFilterProviderSearchHistory, value: providerDrop)) { addBranchFilterProviderSearchHistory(value: providerDrop); } } @@ -1352,16 +1207,12 @@ class AppointmentsVM extends BaseVM { notifyListeners(); } - SelectionModel branchFilterSelectedCategoryId = SelectionModel( - selectedOption: "", selectedId: -1, errorValue: ""); + SelectionModel branchFilterSelectedCategoryId = SelectionModel(selectedOption: "", selectedId: -1, errorValue: ""); - void updateBranchFilterSelectedCategoryId(SelectionModel id, - {bool isForSearch = false}) async { + void updateBranchFilterSelectedCategoryId(SelectionModel id, {bool isForSearch = false}) async { if (isForSearch) { - DropValue categoryDrop = categoryDropList.firstWhere((element) => - element.id == id.selectedId); - if (!ifAlreadyExist( - list: branchFilterCategorySearchHistory, value: categoryDrop)) { + DropValue categoryDrop = categoryDropList.firstWhere((element) => element.id == id.selectedId); + if (!ifAlreadyExist(list: branchFilterCategorySearchHistory, value: categoryDrop)) { addBranchFilterCategorySearchHistory(value: categoryDrop); } } @@ -1395,16 +1246,12 @@ class AppointmentsVM extends BaseVM { notifyListeners(); } - SelectionModel branchFilterSelectedServiceId = SelectionModel( - selectedOption: "", selectedId: -1, errorValue: ""); + SelectionModel branchFilterSelectedServiceId = SelectionModel(selectedOption: "", selectedId: -1, errorValue: ""); - void updateBranchFilterSelectedServiceId(SelectionModel id, - {bool isForSearch = false}) async { + void updateBranchFilterSelectedServiceId(SelectionModel id, {bool isForSearch = false}) async { if (isForSearch) { - DropValue serviceDrop = servicesDropList.firstWhere((element) => - element.id == id.selectedId); - if (!ifAlreadyExist( - list: branchFilterServicesSearchHistory, value: serviceDrop)) { + DropValue serviceDrop = servicesDropList.firstWhere((element) => element.id == id.selectedId); + if (!ifAlreadyExist(list: branchFilterServicesSearchHistory, value: serviceDrop)) { addBranchFilterServicesSearchHistory(value: serviceDrop); } notifyListeners(); @@ -1459,8 +1306,7 @@ class AppointmentsVM extends BaseVM { providersDropList.clear(); setOnlyState(ViewState.busy); - List providers = await branchRepo - .getAllProvidersWitheBasicData(); + List providers = await branchRepo.getAllProvidersWitheBasicData(); for (var element in providers) { providersDropList.add( DropValue(element.id ?? 0, element.providerName ?? "N/A", ""), @@ -1476,9 +1322,7 @@ class AppointmentsVM extends BaseVM { Category category = await branchRepo.fetchBranchCategory(); category.data?.forEach((element) { categoryDropList.add( - DropValue(element.id ?? 0, - ((element.categoryName!.isEmpty ? "N/A" : element.categoryName) ?? - "N/A"), ""), + DropValue(element.id ?? 0, ((element.categoryName!.isEmpty ? "N/A" : element.categoryName) ?? "N/A"), ""), ); }); setState(ViewState.idle); @@ -1488,8 +1332,7 @@ class AppointmentsVM extends BaseVM { if (servicesDropList.isNotEmpty) return; servicesDropList.clear(); setState(ViewState.busy); - Services services = await branchRepo.fetchServicesByCategoryId( - serviceCategoryId: -1); // to get all services + Services services = await branchRepo.fetchServicesByCategoryId(serviceCategoryId: -1); // to get all services for (var element in services.data!) { servicesDropList.add( @@ -1507,8 +1350,7 @@ class AppointmentsVM extends BaseVM { if (itemsDropList.isNotEmpty) return; itemsDropList.clear(); setState(ViewState.busy); - List itemsList = await branchRepo.getServiceItems( - serviceId: -1); // to get all services + List itemsList = await branchRepo.getServiceItems(serviceId: -1); // to get all services for (ItemData element in itemsList) { if (element.name != null && element.name!.isNotEmpty) { @@ -1529,8 +1371,7 @@ class AppointmentsVM extends BaseVM { serviceDeliveryTypesDropList.clear(); setState(ViewState.busy); - List enums = await commonRepo.getEnumTypeValues( - enumTypeID: AppEnums.serviceDeliveryTypeEnumId); + List enums = await commonRepo.getEnumTypeValues(enumTypeID: AppEnums.serviceDeliveryTypeEnumId); for (EnumsModel element in enums) { serviceDeliveryTypesDropList.add( @@ -1563,12 +1404,9 @@ class AppointmentsVM extends BaseVM { } void clearBranchFilterSelections() { - branchFilterSelectedProviderId = - SelectionModel(selectedOption: "", selectedId: -1, errorValue: ""); - branchFilterSelectedCategoryId = - SelectionModel(selectedOption: "", selectedId: -1, errorValue: ""); - branchFilterSelectedServiceId = - SelectionModel(selectedOption: "", selectedId: -1, errorValue: ""); + branchFilterSelectedProviderId = SelectionModel(selectedOption: "", selectedId: -1, errorValue: ""); + branchFilterSelectedCategoryId = SelectionModel(selectedOption: "", selectedId: -1, errorValue: ""); + branchFilterSelectedServiceId = SelectionModel(selectedOption: "", selectedId: -1, errorValue: ""); } Future getBranchesBasedOnFilters() async { @@ -1633,8 +1471,7 @@ class AppointmentsVM extends BaseVM { longitude: AppState().currentLocation.longitude, ); for (var element in providers) { - branchesDropList.add( - DropValue(element.id ?? 0, element.branchName ?? "N/A", "")); + branchesDropList.add(DropValue(element.id ?? 0, element.branchName ?? "N/A", "")); } setState(ViewState.idle); log("branchesDropList: ${branchesDropList.length}"); @@ -1670,8 +1507,7 @@ class AppointmentsVM extends BaseVM { for (var element in appointmentFilterProviderSearchHistory) { providerIdsSelected.add(element.id); } - fetchAllBranchesBySelectedProviderId( - providersIdsList: providerIdsSelected); + fetchAllBranchesBySelectedProviderId(providersIdsList: providerIdsSelected); } if (appointmentFilterProviderSearchHistory.isEmpty) { @@ -1694,16 +1530,12 @@ class AppointmentsVM extends BaseVM { notifyListeners(); } - SelectionModel appointmentFilterSelectedProviderId = SelectionModel( - selectedOption: "", selectedId: -1, errorValue: ""); + SelectionModel appointmentFilterSelectedProviderId = SelectionModel(selectedOption: "", selectedId: -1, errorValue: ""); - void updateAppointmentFilterSelectedProviderId(SelectionModel id, - {bool isForSearch = false}) async { + void updateAppointmentFilterSelectedProviderId(SelectionModel id, {bool isForSearch = false}) async { if (isForSearch) { - DropValue providerDrop = providersDropList.firstWhere((element) => - element.id == id.selectedId); - if (!ifAlreadyExist( - list: appointmentFilterProviderSearchHistory, value: providerDrop)) { + DropValue providerDrop = providersDropList.firstWhere((element) => element.id == id.selectedId); + if (!ifAlreadyExist(list: appointmentFilterProviderSearchHistory, value: providerDrop)) { addAppointmentFilterProviderSearchHistory(value: providerDrop); } } @@ -1737,16 +1569,12 @@ class AppointmentsVM extends BaseVM { notifyListeners(); } - SelectionModel appointmentFilterSelectedBranchId = SelectionModel( - selectedOption: "", selectedId: -1, errorValue: ""); + SelectionModel appointmentFilterSelectedBranchId = SelectionModel(selectedOption: "", selectedId: -1, errorValue: ""); - void updateAppointmentFilterSelectedBranchId(SelectionModel id, - {bool isForSearch = false}) async { + void updateAppointmentFilterSelectedBranchId(SelectionModel id, {bool isForSearch = false}) async { if (isForSearch) { - DropValue branchesDrop = branchesDropList.firstWhere((element) => - element.id == id.selectedId); - if (!ifAlreadyExist( - list: appointmentFilterBranchSearchHistory, value: branchesDrop)) { + DropValue branchesDrop = branchesDropList.firstWhere((element) => element.id == id.selectedId); + if (!ifAlreadyExist(list: appointmentFilterBranchSearchHistory, value: branchesDrop)) { addAppointmentFilterBranchSearchHistory(value: branchesDrop); } } @@ -1780,16 +1608,12 @@ class AppointmentsVM extends BaseVM { notifyListeners(); } - SelectionModel appointmentFilterSelectedCategoryId = SelectionModel( - selectedOption: "", selectedId: -1, errorValue: ""); + SelectionModel appointmentFilterSelectedCategoryId = SelectionModel(selectedOption: "", selectedId: -1, errorValue: ""); - void updateAppointmentFilterSelectedCategoryId(SelectionModel id, - {bool isForSearch = false}) async { + void updateAppointmentFilterSelectedCategoryId(SelectionModel id, {bool isForSearch = false}) async { if (isForSearch) { - DropValue categoryDrop = categoryDropList.firstWhere((element) => - element.id == id.selectedId); - if (!ifAlreadyExist( - list: appointmentFilterCategorySearchHistory, value: categoryDrop)) { + DropValue categoryDrop = categoryDropList.firstWhere((element) => element.id == id.selectedId); + if (!ifAlreadyExist(list: appointmentFilterCategorySearchHistory, value: categoryDrop)) { addAppointmentFilterCategorySearchHistory(value: categoryDrop); } } @@ -1821,16 +1645,12 @@ class AppointmentsVM extends BaseVM { notifyListeners(); } - SelectionModel appointmentFilterSelectedServiceId = SelectionModel( - selectedOption: "", selectedId: -1, errorValue: ""); + SelectionModel appointmentFilterSelectedServiceId = SelectionModel(selectedOption: "", selectedId: -1, errorValue: ""); - void updateAppointmentFilterSelectedServiceId(SelectionModel id, - {bool isForSearch = false}) async { + void updateAppointmentFilterSelectedServiceId(SelectionModel id, {bool isForSearch = false}) async { if (isForSearch) { - DropValue servicesDrop = servicesDropList.firstWhere((element) => - element.id == id.selectedId); - if (!ifAlreadyExist( - list: appointmentFilterServicesSearchHistory, value: servicesDrop)) { + DropValue servicesDrop = servicesDropList.firstWhere((element) => element.id == id.selectedId); + if (!ifAlreadyExist(list: appointmentFilterServicesSearchHistory, value: servicesDrop)) { addAppointmentFilterServicesSearchHistory(value: servicesDrop); } } @@ -1862,16 +1682,12 @@ class AppointmentsVM extends BaseVM { notifyListeners(); } - SelectionModel appointmentFilterSelectedItemId = SelectionModel( - selectedOption: "", selectedId: -1, errorValue: ""); + SelectionModel appointmentFilterSelectedItemId = SelectionModel(selectedOption: "", selectedId: -1, errorValue: ""); - void updateAppointmentFilterSelectedItemId(SelectionModel id, - {bool isForSearch = false}) async { + void updateAppointmentFilterSelectedItemId(SelectionModel id, {bool isForSearch = false}) async { if (isForSearch) { - DropValue itemsDrop = itemsDropList.firstWhere((element) => - element.id == id.selectedId); - if (!ifAlreadyExist( - list: appointmentFilterItemsSearchHistory, value: itemsDrop)) { + DropValue itemsDrop = itemsDropList.firstWhere((element) => element.id == id.selectedId); + if (!ifAlreadyExist(list: appointmentFilterItemsSearchHistory, value: itemsDrop)) { addAppointmentFilterItemsSearchHistory(value: itemsDrop); } } @@ -1908,8 +1724,7 @@ class AppointmentsVM extends BaseVM { updateAppointmentFiltersCounter(appointmentFiltersCounter + 1); } - if (!ifAlreadyExistForStringValue( - list: appointmentFilterCustomerNameSearchHistory, value: value)) { + if (!ifAlreadyExistForStringValue(list: appointmentFilterCustomerNameSearchHistory, value: value)) { appointmentFilterCustomerNameSearchHistory.add(value); currentCustomerNameFilter = ""; } @@ -1946,8 +1761,7 @@ class AppointmentsVM extends BaseVM { updateAppointmentFiltersCounter(appointmentFiltersCounter + 1); } - if (!ifAlreadyExistForStringValue( - list: appointmentFilterMobilePhoneSearchHistory, value: value)) { + if (!ifAlreadyExistForStringValue(list: appointmentFilterMobilePhoneSearchHistory, value: value)) { appointmentFilterMobilePhoneSearchHistory.add(value); currentMobilePhoneFilter = ""; } @@ -1981,15 +1795,12 @@ class AppointmentsVM extends BaseVM { notifyListeners(); } - SelectionModel appointmentFilterSelectedServiceDeliveryId = SelectionModel( - selectedOption: "", selectedId: -1, errorValue: ""); + SelectionModel appointmentFilterSelectedServiceDeliveryId = SelectionModel(selectedOption: "", selectedId: -1, errorValue: ""); - void updateAppointmentFilterSelectedServiceDeliveryId(SelectionModel id, - {bool isForSearch = false}) async { + void updateAppointmentFilterSelectedServiceDeliveryId(SelectionModel id, {bool isForSearch = false}) async { if (isForSearch) { DropValue itemsDrop = serviceDeliveryTypesDropList.firstWhere((element) => element.id == id.selectedId); - if (!ifAlreadyExist(list: appointmentFilterServiceDeliverySearchHistory, - value: itemsDrop)) { + if (!ifAlreadyExist(list: appointmentFilterServiceDeliverySearchHistory, value: itemsDrop)) { addAppointmentFilterServiceDeliverySearchHistory(value: itemsDrop); } } @@ -1998,18 +1809,12 @@ class AppointmentsVM extends BaseVM { } void clearAppointmentFilterSelections() { - appointmentFilterSelectedProviderId = - SelectionModel(selectedOption: "", selectedId: -1, errorValue: ""); - appointmentFilterSelectedCategoryId = - SelectionModel(selectedOption: "", selectedId: -1, errorValue: ""); - appointmentFilterSelectedServiceId = - SelectionModel(selectedOption: "", selectedId: -1, errorValue: ""); - appointmentFilterSelectedItemId = - SelectionModel(selectedOption: "", selectedId: -1, errorValue: ""); - appointmentFilterSelectedServiceDeliveryId = - SelectionModel(selectedOption: "", selectedId: -1, errorValue: ""); - appointmentFilterSelectedBranchId = - SelectionModel(selectedOption: "", selectedId: -1, errorValue: ""); + appointmentFilterSelectedProviderId = SelectionModel(selectedOption: "", selectedId: -1, errorValue: ""); + appointmentFilterSelectedCategoryId = SelectionModel(selectedOption: "", selectedId: -1, errorValue: ""); + appointmentFilterSelectedServiceId = SelectionModel(selectedOption: "", selectedId: -1, errorValue: ""); + appointmentFilterSelectedItemId = SelectionModel(selectedOption: "", selectedId: -1, errorValue: ""); + appointmentFilterSelectedServiceDeliveryId = SelectionModel(selectedOption: "", selectedId: -1, errorValue: ""); + appointmentFilterSelectedBranchId = SelectionModel(selectedOption: "", selectedId: -1, errorValue: ""); } void clearAppointmentFilters() { @@ -2025,12 +1830,9 @@ class AppointmentsVM extends BaseVM { clearAppointmentFilterSelections(); if (AppState().currentAppType == AppType.provider) { - applyFilterOnAppointmentsVMForProviders( - appointmentStatusEnum: AppointmentStatusEnum.allAppointments, - isNeedCustomerFilter: true); + applyFilterOnAppointmentsVMForProviders(appointmentStatusEnum: AppointmentStatusEnum.allAppointments, isNeedCustomerFilter: true); } else { - applyFilterOnAppointmentsVMForCustomers( - appointmentStatusEnum: AppointmentStatusEnum.allAppointments); + applyFilterOnAppointmentsVMForCustomers(appointmentStatusEnum: AppointmentStatusEnum.allAppointments); } notifyListeners(); } @@ -2069,8 +1871,7 @@ class AppointmentsVM extends BaseVM { } } - List myAppointments = await appointmentRepo - .getMyAppointmentsForCustomersByFilters( + List myAppointments = await appointmentRepo.getMyAppointmentsForCustomersByFilters( providerIdsList: providersIdsList, categoryIdsList: categoryIdsList, serviceIdsList: servicesIdsList, @@ -2125,8 +1926,7 @@ class AppointmentsVM extends BaseVM { log("customerPhonesList: $customerPhonesList"); log("customerDeliveryTypesList: $customerDeliveryTypesList"); log("servicesIdsList: $servicesIdsList"); - List myAppointments = await appointmentRepo - .getMyAppointmentsForProvidersByFilters( + List myAppointments = await appointmentRepo.getMyAppointmentsForProvidersByFilters( branchID: branchID, customerNamesList: customerNamesList, customerPhonesList: customerPhonesList, @@ -2225,8 +2025,7 @@ class AppointmentsVM extends BaseVM { Future getReviewsByBranchId({required int serviceProviderBranchID}) async { setState(ViewState.busy); try { - currentBranchReviews = await branchRepo.getBranchRatings( - serviceProviderBranchID: serviceProviderBranchID); + currentBranchReviews = await branchRepo.getBranchRatings(serviceProviderBranchID: serviceProviderBranchID); } catch (e, s) { logger.e(s); setState(ViewState.idle); @@ -2273,4 +2072,10 @@ class AppointmentsVM extends BaseVM { // .whenComplete(() => navigateWithName(context, AppRoutes.chatView, arguments: chatViewArguments)); // } } + + @override + void dispose() { + branchesFilterScrollController.dispose(); + super.dispose(); + } } diff --git a/lib/view_models/chat_view_model.dart b/lib/view_models/chat_view_model.dart index a672365..70e3e3d 100644 --- a/lib/view_models/chat_view_model.dart +++ b/lib/view_models/chat_view_model.dart @@ -76,6 +76,35 @@ class ChatVM extends BaseVM { bool isUserOnChatScreen = false; + // Track currently active chat + int? currentAdsId; + String? currentReceiverId; + int? currentRequestId; + + // Update current chat context + void setCurrentChatContext({int? adsId, String? receiverId, int? requestId}) { + currentAdsId = adsId; + currentReceiverId = receiverId; + currentRequestId = requestId; + } + + // Clear current chat context + void clearCurrentChatContext() { + currentAdsId = null; + currentReceiverId = null; + currentRequestId = null; + } + + // Check if currently on a specific ad chat + bool isOnAdChat(int adsId, String receiverId) { + return isUserOnChatScreen && currentAdsId == adsId && currentReceiverId == receiverId; + } + + // Check if currently on a specific request chat + bool isOnRequestChat(int requestId, String receiverId) { + return isUserOnChatScreen && currentRequestId == requestId && currentReceiverId == receiverId; + } + final ScrollController scrollController = ScrollController(); void scrollChatDown() { @@ -83,11 +112,20 @@ class ChatVM extends BaseVM { return; } if (scrollController.hasClients) { - scrollController.animateTo( - scrollController.position.maxScrollExtent + 200, // for the text field - duration: const Duration(seconds: 1), - curve: Curves.fastOutSlowIn, - ); + // Check if attached to exactly one scroll view + try { + if (scrollController.positions.length == 1) { + scrollController.animateTo( + scrollController.position.maxScrollExtent + 200, // for the text field + duration: const Duration(seconds: 1), + curve: Curves.fastOutSlowIn, + ); + } else { + log("⚠️ ScrollController attached to ${scrollController.positions.length} scroll views, skipping scroll"); + } + } catch (e) { + log("Error scrolling chat: $e"); + } } } @@ -957,7 +995,15 @@ class ChatVM extends BaseVM { } Future onNewMessageReceivedForAds({required List messages}) async { + final myUserId = AppState().getUser.data?.userInfo?.userId?.toString(); + for (var msg in messages) { + // Skip if this is my own message echoed back from server + if (myUserId != null && msg.senderUserID == myUserId) { + log("Skipping own message echoed from server (sender: ${msg.senderUserID})"); + continue; + } + // Check if message already exists to prevent duplicates bool messageExists = false; @@ -1005,8 +1051,8 @@ class ChatVM extends BaseVM { 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)); + // Wait longer for connection to fully establish and register with server + await Future.delayed(const Duration(milliseconds: 1500)); // Check again after attempt if (hubConnection == null || hubConnection!.state != HubConnectionState.connected) { @@ -1015,6 +1061,14 @@ class ChatVM extends BaseVM { notifyListeners(); return false; } + + // Extra verification: ensure connection ID is available + if (hubConnection!.connectionId == null) { + logger.w("Connection ID not yet available, waiting a bit more..."); + await Future.delayed(const Duration(milliseconds: 500)); + } + + logger.i("Connection ready! ConnectionId: ${hubConnection!.connectionId}"); } if (hubConnection!.state == HubConnectionState.connected) { @@ -1022,7 +1076,7 @@ class ChatVM extends BaseVM { final name = AppState().getUser.data!.userInfo!.firstName.toString(); final obj = { "ReceiverUserID": receiverId, - // "SenderUserID": userId, + "SenderUserID": userId, // ✅ Send SenderUserID so backend knows who sent it "MessageType": chatMessageType.getIdFromChatMessageTypeEnum(), "ChatText": message, "AdsID": adId, diff --git a/lib/view_models/dashboard_view_model_customer.dart b/lib/view_models/dashboard_view_model_customer.dart index fa8492b..8dfee34 100644 --- a/lib/view_models/dashboard_view_model_customer.dart +++ b/lib/view_models/dashboard_view_model_customer.dart @@ -50,18 +50,18 @@ class DashboardVmCustomer extends BaseVM with WidgetsBindingObserver { switch (state) { case AppLifecycleState.resumed: - // App came back to foreground - ensure SignalR is connected + // 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) + // 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 + // Other states - no action needed break; } } @@ -71,7 +71,7 @@ class DashboardVmCustomer extends BaseVM with WidgetsBindingObserver { int selectedNavbarBarIndex = 2; void onNavbarTapped(int index) async { - logger.i(AppState().getDeviceToken); + log("token: ${AppState().getDeviceToken}"); selectedNavbarBarIndex = index; notifyListeners(); } @@ -119,19 +119,28 @@ class DashboardVmCustomer extends BaseVM with WidgetsBindingObserver { await appointmentsVM.applyFilterOnBranches(index: 0); // to get all branches! await appointmentsVM.getMyRecentBranches(); // to get my recent branches - if (dashboardRouteEnum != DashboardRouteEnum.fromAdsPayment && - dashboardRouteEnum != DashboardRouteEnum.fromAdsSubmit && - dashboardRouteEnum != DashboardRouteEnum.fromReserveAdsPayment) { + if (dashboardRouteEnum != DashboardRouteEnum.fromAdsPayment && dashboardRouteEnum != DashboardRouteEnum.fromAdsSubmit && dashboardRouteEnum != DashboardRouteEnum.fromReserveAdsPayment) { await adVM.getMyAds(); await adVM.getExploreAds(); } - - // Handle reserve ads payment completion + + // Handle different payment completion scenarios if (dashboardRouteEnum == DashboardRouteEnum.fromReserveAdsPayment) { adVM.updateIsExploreAds(false); - onNavbarTapped(2); // Go to My Ads tab (customer has different tab index) + onNavbarTapped(3); // Go to My Ads tab await adVM.applyFilterOnMyAds(adPostStatusEnum: AdPostStatus.reserved); log("✅ Successfully navigated to Reserved Ads after payment"); + } else if (dashboardRouteEnum == DashboardRouteEnum.fromAdsPayment) { + adVM.updateIsExploreAds(false); + onNavbarTapped(3); // Go to My Ads tab + await adVM.applyFilterOnMyAds(adPostStatusEnum: AdPostStatus.pendingForPost); + log("✅ Successfully navigated to My Ads after payment"); + } else if (dashboardRouteEnum == DashboardRouteEnum.fromRequestPayment) { + onNavbarTapped(4); // Go to Requests tab + log("✅ Successfully navigated to Requests after payment"); + } else if (dashboardRouteEnum == DashboardRouteEnum.fromAppointmentPayment) { + onNavbarTapped(1); // Go to Appointments tab + log("✅ Successfully navigated to Appointments after payment"); } await requestsVM.getRequests(); diff --git a/lib/view_models/dashboard_view_model_provider.dart b/lib/view_models/dashboard_view_model_provider.dart index c7c5261..d0b1743 100644 --- a/lib/view_models/dashboard_view_model_provider.dart +++ b/lib/view_models/dashboard_view_model_provider.dart @@ -59,18 +59,18 @@ class DashboardVMProvider extends BaseVM with WidgetsBindingObserver { switch (state) { case AppLifecycleState.resumed: - // App came back to foreground - ensure SignalR is connected + // 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) + // 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 + // Other states - no action needed break; } } @@ -80,7 +80,14 @@ class DashboardVMProvider extends BaseVM with WidgetsBindingObserver { int selectedNavbarBarIndex = 2; void onNavbarTapped(int index) async { - logger.i(AppState().getDeviceToken); + log("token: ${AppState().getDeviceToken.toString()}"); + + // Block access to Requests tab (index 4) for Individual Providers + // Only Dealerships can access Special Requests + if (index == 4 && AppState().userType != UserType.providerDealer) { + logger.w("Access denied: Individual providers cannot access Special Requests"); + return; + } selectedNavbarBarIndex = index; notifyListeners(); diff --git a/lib/view_models/payment_view_model.dart b/lib/view_models/payment_view_model.dart index 7ebcde2..5677ee7 100644 --- a/lib/view_models/payment_view_model.dart +++ b/lib/view_models/payment_view_model.dart @@ -26,6 +26,9 @@ class PaymentVM extends ChangeNotifier { PaymentMethods selectedPaymentMethod = PaymentMethods.mada; PaymentTypes currentPaymentType = PaymentTypes.ads; + // Store AdVM reference for updating local state after payment + AdVM? _cachedAdVM; + int currentAdId = -1; void updateCurrentAdId({required int id}) { @@ -57,6 +60,13 @@ class PaymentVM extends ChangeNotifier { } Future onContinuePressed(BuildContext context, {required PaymentTypes paymentType}) async { + // Cache AdVM reference for updating local state after payment + try { + _cachedAdVM = context.read(); + } catch (e) { + log("Could not get AdVM reference: $e"); + } + switch (selectedPaymentMethod) { case PaymentMethods.mada: break; @@ -87,7 +97,7 @@ class PaymentVM extends ChangeNotifier { // Utils.hideLoading(context); // Commented out: matching pair with showLoading log("isPaid: $isPaid"); - + if (!isPaid) { // Utils.showToast(LocaleKeys.paymentFailed.tr()); // pop(context); @@ -95,7 +105,7 @@ class PaymentVM extends ChangeNotifier { } else { Utils.showToast(LocaleKeys.paymentSuccessful.tr()); } - + // Call onVerified callback - success methods handle their own context checks onVerified(); } catch (e) { @@ -198,96 +208,129 @@ class PaymentVM extends ChangeNotifier { } void onRequestPaymentSuccess(BuildContext context) { - log("onRequestPaymentSuccess"); - // Check if context is still mounted before using it - if (!context.mounted) { - log("Context not mounted, using navigatorKey instead"); - // Use navigatorKey to navigate without context dependency - navigatorKey.currentState?.pushNamedAndRemoveUntil( + log("🔵 onRequestPaymentSuccess called"); + + // Wait a bit for browser to close and context to be ready + Future.delayed(const Duration(milliseconds: 500), () { + log("Attempting navigation after delay..."); + final nav = navigatorKey.currentState; + if (nav == null) { + log("❌ navigatorKey.currentState is still null after delay"); + return; + } + + nav.pushNamedAndRemoveUntil( AppRoutes.dashboard, (route) => false, + arguments: DashboardRouteEnum.fromRequestPayment, ); - return; - } - - if (AppState().currentAppType == AppType.provider) { - context.read().onNavbarTapped(4); - } else { - context.read().onNavbarTapped(4); - } - navigateReplaceWithNameUntilRoute(context, AppRoutes.dashboard); + + log("✅ Navigation executed successfully"); + }); } void onAdsPaymentSuccess(BuildContext context) { - log("onAdsPaymentSuccess"); - // Check if context is still mounted before using it - if (!context.mounted) { - log("Context not mounted, using navigatorKey instead"); - // Use navigatorKey to navigate without context dependency - navigatorKey.currentState?.pushNamedAndRemoveUntil( + log("🔵 onAdsPaymentSuccess called"); + + // Update local ad state immediately so UI reflects payment success + if (_cachedAdVM != null && currentAdId != -1) { + try { + // Find and update the ad in myAds list + var adIndex = _cachedAdVM!.myAds.indexWhere((ad) => ad.id == currentAdId); + if (adIndex != -1) { + _cachedAdVM!.myAds[adIndex].adPostStatus = AdPostStatus.active; + _cachedAdVM!.notifyListeners(); + log("✅ Local ad state updated - status changed to active"); + } else { + log("⚠️ Ad not found in myAds list"); + } + } catch (e) { + log("⚠️ Could not update local ad state: $e"); + } + } + + // Wait a bit for browser to close and context to be ready + Future.delayed(const Duration(milliseconds: 500), () { + log("Attempting navigation after delay..."); + final nav = navigatorKey.currentState; + if (nav == null) { + log("❌ navigatorKey.currentState is still null after delay"); + return; + } + + nav.pushNamedAndRemoveUntil( AppRoutes.dashboard, (route) => false, arguments: DashboardRouteEnum.fromAdsPayment, ); - return; - } - - context.read().updateIsExploreAds(false); - if (AppState().currentAppType == AppType.provider) { - context.read().onNavbarTapped(3); - context.read().applyFilterOnMyAds(adPostStatusEnum: AdPostStatus.active); - } else { - context.read().onNavbarTapped(3); - context.read().applyFilterOnMyAds(adPostStatusEnum: AdPostStatus.pendingForPost); - } - navigateReplaceWithNameUntilRoute(context, AppRoutes.dashboard, arguments: DashboardRouteEnum.fromAdsPayment); + + log("✅ Navigation executed successfully"); + }); } void onAdReservePaymentSuccess(BuildContext context) { - log("onAdReservePaymentSuccess - Reserve ads payment completed"); - - // Navigate to dashboard with reserved ads route argument - // The dashboard's onRefresh method will handle setting up the reserved filter - log("Navigating to dashboard with reserved ads filter"); - navigatorKey.currentState?.pushNamedAndRemoveUntil( - AppRoutes.dashboard, - (route) => false, - arguments: DashboardRouteEnum.fromReserveAdsPayment, - ); + log("🔵 onAdReservePaymentSuccess called"); - log("✅ Navigation initiated - Dashboard will handle filter setup"); + // Wait a bit for browser to close and context to be ready + Future.delayed(const Duration(milliseconds: 500), () { + log("Attempting navigation after delay..."); + final nav = navigatorKey.currentState; + if (nav == null) { + log("❌ navigatorKey.currentState is still null after delay"); + return; + } + + nav.pushNamedAndRemoveUntil( + AppRoutes.dashboard, + (route) => false, + arguments: DashboardRouteEnum.fromReserveAdsPayment, + ); + + log("✅ Navigation executed successfully"); + }); } void onSubscriptionPaymentSuccess() { - log("onSubscriptionPaymentSuccess"); - // Use navigatorKey to navigate without context dependency - // This works even after payment browser is closed and context is unmounted - navigatorKey.currentState?.pushNamedAndRemoveUntil( - AppRoutes.dashboard, - (route) => false, - ); - // context.read().getSubscriptionBySP(AppState().getUser.data?.userInfo?.providerId.toString() ?? "", true); + log("🔵 onSubscriptionPaymentSuccess called"); + + // Wait a bit for browser to close and context to be ready + Future.delayed(const Duration(milliseconds: 500), () { + log("Attempting navigation after delay..."); + final nav = navigatorKey.currentState; + if (nav == null) { + log("❌ navigatorKey.currentState is still null after delay"); + return; + } + + nav.pushNamedAndRemoveUntil( + AppRoutes.dashboard, + (route) => false, + ); + + log("✅ Navigation executed successfully"); + }); } void onAppointmentSuccess(BuildContext context) { - log("onAppointmentSuccess"); - // Check if context is still mounted before using it - if (!context.mounted) { - log("Context not mounted, using navigatorKey instead"); - // Use navigatorKey to navigate without context dependency - navigatorKey.currentState?.pushNamedAndRemoveUntil( + log("🔵 onAppointmentSuccess called"); + + // Wait a bit for browser to close and context to be ready + Future.delayed(const Duration(milliseconds: 500), () { + log("Attempting navigation after delay..."); + final nav = navigatorKey.currentState; + if (nav == null) { + log("❌ navigatorKey.currentState is still null after delay"); + return; + } + + nav.pushNamedAndRemoveUntil( AppRoutes.dashboard, (route) => false, + arguments: DashboardRouteEnum.fromAppointmentPayment, ); - return; - } - - if (AppState().currentAppType == AppType.provider) { - context.read().onNavbarTapped(1); - } else { - context.read().onNavbarTapped(1); - } - navigateReplaceWithNameUntilRoute(context, AppRoutes.dashboard); + + log("✅ Navigation executed successfully"); + }); } Future onVisaCardSelected(BuildContext context, PaymentTypes paymentType, {Function()? onExit}) async { diff --git a/lib/view_models/requests_view_model.dart b/lib/view_models/requests_view_model.dart index 7b8e234..400fd2c 100644 --- a/lib/view_models/requests_view_model.dart +++ b/lib/view_models/requests_view_model.dart @@ -76,6 +76,14 @@ class RequestsVM extends BaseVM { notifyListeners(); } + // Loading state for refreshing after accepting offer + bool isRefreshingAfterAccept = false; + + void updateIsRefreshingAfterAccept(bool value) { + isRefreshingAfterAccept = value; + notifyListeners(); + } + String addressSparePartRequestDelivery = ""; String additionalAddressSparePartRequestDelivery = ""; diff --git a/lib/view_models/user_view_model.dart b/lib/view_models/user_view_model.dart index 4a091a8..e5e8e9d 100644 --- a/lib/view_models/user_view_model.dart +++ b/lib/view_models/user_view_model.dart @@ -749,7 +749,7 @@ class UserVM extends BaseVM { } else { context.setLocale(const Locale('ar', 'SA')); } - log ("updated status: ${AppState().isArabic(context)}"); + log("updated status: ${AppState().isArabic(context)}"); // notifyListeners(); } diff --git a/lib/views/advertisement/ad_creation_steps/ad_review_containers.dart b/lib/views/advertisement/ad_creation_steps/ad_review_containers.dart index ab785b2..605dd4e 100644 --- a/lib/views/advertisement/ad_creation_steps/ad_review_containers.dart +++ b/lib/views/advertisement/ad_creation_steps/ad_review_containers.dart @@ -110,7 +110,7 @@ class VehicleDetailsReview extends StatelessWidget { 16.height, SingleDetailWidget(text: adVM.financeAvailableStatus ? LocaleKeys.yes.tr() : LocaleKeys.no.tr(), type: LocaleKeys.financeAvailable.tr()), 16.height, - SingleDetailWidget(text: adVM.isInUsed == 1 ? LocaleKeys.yes.tr() : LocaleKeys.no.tr(), type: LocaleKeys.currentlyInUse.tr()), + SingleDetailWidget(text: adVM.isInUsed == 1 ? LocaleKeys.yes.tr() : (adVM.isInUsed == 0 ? LocaleKeys.no.tr() : "-"), type: LocaleKeys.currentlyInUse.tr()), 16.height, ], ), diff --git a/lib/views/advertisement/ad_creation_steps/vehicle_details_container.dart b/lib/views/advertisement/ad_creation_steps/vehicle_details_container.dart index 7461c5b..bf66f38 100644 --- a/lib/views/advertisement/ad_creation_steps/vehicle_details_container.dart +++ b/lib/views/advertisement/ad_creation_steps/vehicle_details_container.dart @@ -163,7 +163,10 @@ class VehicleDetails extends StatelessWidget { Builder(builder: (context) { List vehicleCategoriesDrop = []; for (var element in adVM.vehicleCategories) { - vehicleCategoriesDrop.add(DropValue(element.id?.toInt() ?? 0, element.category ?? "", "")); + // Skip invalid items (id: 0 or null, empty category) + if (element.id != null && element.id! > 0 && (element.category?.isNotEmpty ?? false)) { + vehicleCategoriesDrop.add(DropValue(element.id!.toInt(), element.category!, "")); + } } return DropdownField( (DropValue value) => adVM.updateSelectionVehicleCategoryId(SelectionModel(selectedId: value.id, selectedOption: value.value)), @@ -177,7 +180,13 @@ class VehicleDetails extends StatelessWidget { Builder(builder: (context) { List vehicleMileageDrop = []; for (var element in adVM.vehicleMileages) { - vehicleMileageDrop.add(DropValue(element.id?.toInt() ?? 0, "${element.mileageStart ?? ""} - ${element.mileageEnd ?? ""}", "")); + // Skip invalid items + if (element.id != null && element.id! > 0) { + String label = "${element.mileageStart ?? ""} - ${element.mileageEnd ?? ""}"; + if (label.trim() != "-") { + vehicleMileageDrop.add(DropValue(element.id!.toInt(), label, "")); + } + } } return DropdownField( (DropValue value) => adVM.updateSelectionVehicleMileageId(SelectionModel(selectedId: value.id, selectedOption: value.value)), @@ -191,7 +200,10 @@ class VehicleDetails extends StatelessWidget { Builder(builder: (context) { List vehicleTransmissionsDrop = []; for (var element in adVM.vehicleTransmissions) { - vehicleTransmissionsDrop.add(DropValue(element.id?.toInt() ?? 0, element.transmission ?? "", "")); + // Skip invalid items + if (element.id != null && element.id! > 0 && (element.transmission?.isNotEmpty ?? false)) { + vehicleTransmissionsDrop.add(DropValue(element.id!.toInt(), element.transmission!, "")); + } } return DropdownField( (DropValue value) => adVM.updateSelectionVehicleTransmissionId(SelectionModel(selectedId: value.id, selectedOption: value.value)), @@ -206,7 +218,10 @@ class VehicleDetails extends StatelessWidget { Builder(builder: (context) { List vehicleCountriesDrop = []; for (var element in adVM.vehicleCountries) { - vehicleCountriesDrop.add(DropValue(element.id?.toInt() ?? 0, element.countryName ?? "", "")); + // Skip invalid items + if (element.id != null && element.id! > 0 && (element.countryName?.isNotEmpty ?? false)) { + vehicleCountriesDrop.add(DropValue(element.id!.toInt(), element.countryName!, "")); + } } return DropdownField( (DropValue value) => adVM.updateSelectionVehicleCountryId(SelectionModel(selectedOption: value.value, selectedId: value.id)), @@ -227,7 +242,10 @@ class VehicleDetails extends StatelessWidget { Builder(builder: (context) { List vehicleCitiesDrop = []; for (var element in adVM.vehicleCities) { - vehicleCitiesDrop.add(DropValue(element.id?.toInt() ?? 0, element.cityName ?? "", "")); + // Skip invalid items + if (element.id != null && element.id! > 0 && (element.cityName?.isNotEmpty ?? false)) { + vehicleCitiesDrop.add(DropValue(element.id!.toInt(), element.cityName!, "")); + } } return DropdownField( (DropValue value) => adVM.updateSelectionVehicleCityId(SelectionModel(selectedId: value.id, selectedOption: value.value)), @@ -247,8 +265,8 @@ class VehicleDetails extends StatelessWidget { (DropValue value) => adVM.updateIsAccidentFree(value.id == 1), list: accidentFreeDrop, dropdownValue: adVM.isAccidentFreeSet ? DropValue(adVM.isAccidentFree ? 1 : 0, adVM.isAccidentFree ? LocaleKeys.yes.tr() : LocaleKeys.no.tr(), "") : null, - hint: LocaleKeys.hasYourCarHadAccident.tr(), - errorValue: "", + hint: LocaleKeys.accidentFree.tr(), + errorValue: adVM.accidentFreeError, ); }), 8.height, @@ -267,13 +285,15 @@ class VehicleDetails extends StatelessWidget { DropValue(1, LocaleKeys.yes.tr(), ""), DropValue(0, LocaleKeys.no.tr(), ""), ], - dropdownValue: DropValue( - adVM.isInUsed, - adVM.isInUsed == 1 ? LocaleKeys.yes.tr() : LocaleKeys.no.tr(), - "", - ), + dropdownValue: adVM.isInUsed != null + ? DropValue( + adVM.isInUsed!, + adVM.isInUsed == 1 ? LocaleKeys.yes.tr() : LocaleKeys.no.tr(), + "", + ) + : null, hint: LocaleKeys.canMileageIncrease.tr(), - errorValue: "", + errorValue: adVM.isInUsedError, ), ], ], 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 2e6c277..90131c2 100644 --- a/lib/views/advertisement/ads_detail_view/ads_detail_view.dart +++ b/lib/views/advertisement/ads_detail_view/ads_detail_view.dart @@ -601,7 +601,9 @@ class _FavoriteAdButtonState extends State with SingleTickerPr _animationController.forward().then((_) => _animationController.reverse()); bool success = await context.read().toggleAdFavorite( - adsID: widget.adDetails.id!, + adsID: isFavorite && widget.adDetails.favouriteAdID != null + ? widget.adDetails.favouriteAdID! // Use favoriteAdsID when unfavoriting + : widget.adDetails.id!, // Use regular ad ID when favoriting isFavorite: isFavorite, ); diff --git a/lib/views/advertisement/components/ads_list_widget.dart b/lib/views/advertisement/components/ads_list_widget.dart index d724cfb..129cd7c 100644 --- a/lib/views/advertisement/components/ads_list_widget.dart +++ b/lib/views/advertisement/components/ads_list_widget.dart @@ -193,11 +193,18 @@ class _AdCardState extends State { ); }, ) - : widget.adDetails.vehicle!.image!.first.imageUrl.buildNetworkImage( - width: 100, - height: 100, - fit: BoxFit.cover, - ), + : (widget.adDetails.vehicle?.image?.isNotEmpty ?? false) + ? widget.adDetails.vehicle!.image!.first.imageUrl.buildNetworkImage( + width: 100, + height: 100, + fit: BoxFit.cover, + ) + : Container( + width: 100, + height: 100, + color: Colors.grey[300], + child: Icon(Icons.image_not_supported, color: Colors.grey[600], size: 40), + ), ), ), // Image indicator dots diff --git a/lib/views/advertisement/select_ad_type_view.dart b/lib/views/advertisement/select_ad_type_view.dart index 5961765..a2fb394 100644 --- a/lib/views/advertisement/select_ad_type_view.dart +++ b/lib/views/advertisement/select_ad_type_view.dart @@ -122,7 +122,7 @@ class SelectAdTypeView extends StatelessWidget { children: [ "${LocaleKeys.duration.tr()}: ".toText(fontSize: 12, color: MyColors.lightTextColor, isBold: true), 2.width, - LocaleKeys.validUntilSubscriptionExpiration.tr().toText(fontSize: 13), + LocaleKeys.validUntilSubscriptionExpiration.tr().toText(fontSize: 10), ], ).paddingOnly(top: 5, bottom: 5), ] else ...[ diff --git a/lib/views/appointments/widgets/common_appointment_slider_widget.dart b/lib/views/appointments/widgets/common_appointment_slider_widget.dart index 78a7631..4ed8f6a 100644 --- a/lib/views/appointments/widgets/common_appointment_slider_widget.dart +++ b/lib/views/appointments/widgets/common_appointment_slider_widget.dart @@ -177,7 +177,7 @@ class AppointmentHomeTileWidget extends StatelessWidget { .toText(color: MyColors.lightTextColor, fontSize: 12, letterSpacing: -0.48, height: 19 / 12), ], ), - 8.height, + 4.height, !isForCustomerHome ? Row( crossAxisAlignment: CrossAxisAlignment.end, diff --git a/lib/views/chat/chat_view.dart b/lib/views/chat/chat_view.dart index 011305f..f239962 100644 --- a/lib/views/chat/chat_view.dart +++ b/lib/views/chat/chat_view.dart @@ -14,7 +14,6 @@ import 'package:mc_common_app/view_models/chat_view_model.dart'; import 'package:mc_common_app/view_models/dashboard_view_model_provider.dart'; import 'package:mc_common_app/view_models/payment_view_model.dart'; import 'package:mc_common_app/view_models/requests_view_model.dart'; -import 'package:mc_common_app/views/advertisement/ads_buyer_chats_view.dart'; import 'package:mc_common_app/views/advertisement/components/picked_images_container_widget.dart'; import 'package:mc_common_app/views/chat/widgets/chat_bottom_sheets.dart'; import 'package:mc_common_app/views/chat/widgets/chat_message_widget.dart'; @@ -54,6 +53,20 @@ class _ChatViewState extends State { } chatVM = context.read(); chatVM.isUserOnChatScreen = true; + + // Set current chat context for notification filtering + if (chatTypeEnum == ChatTypeEnum.ads && chatViewArgumentsForAd != null) { + chatVM.setCurrentChatContext( + adsId: chatViewArgumentsForAd!.adsID, + receiverId: chatViewArgumentsForAd!.receiverUserID, + ); + } else if (chatTypeEnum == ChatTypeEnum.requestOffer && chatViewArgumentsForRequest != null) { + chatVM.setCurrentChatContext( + requestId: chatViewArgumentsForRequest!.requestId, + receiverId: chatViewArgumentsForRequest!.receiverId, + ); + } + WidgetsBinding.instance.addPostFrameCallback((_) { if (chatVM.scrollController.hasClients) { chatVM.scrollChatDown(); @@ -65,6 +78,16 @@ class _ChatViewState extends State { @override void dispose() { chatVM.isUserOnChatScreen = false; + chatVM.clearCurrentChatContext(); // Clear chat context when leaving + // Detach scroll controller to prevent "attached to multiple scroll views" error + if (chatVM.scrollController.hasClients) { + try { + // Force detach by jumping to current position (this detaches from current view) + chatVM.scrollController.jumpTo(chatVM.scrollController.offset); + } catch (e) { + // Ignore errors during dispose + } + } super.dispose(); } @@ -213,13 +236,16 @@ class _ChatViewState extends State { maxWidth: double.infinity, margin: const EdgeInsets.symmetric(vertical: 5, horizontal: 10), maxHeight: 55, - title: LocaleKeys.reviewAndPayment.tr(), + title: requestVM.isRefreshingAfterAccept ? LocaleKeys.loading.tr() : LocaleKeys.reviewAndPayment.tr(), isBold: false, - onPressed: () { - context.read().updateRequestId(id: requestVM.currentSelectedRequest!.id); - requestVM.selectedDeliveryOptionEnum = null; - navigateWithName(context, AppRoutes.reviewRequestOffer, arguments: PaymentTypes.request); - }, + isLoading: requestVM.isRefreshingAfterAccept, + onPressed: requestVM.isRefreshingAfterAccept + ? null + : () { + context.read().updateRequestId(id: requestVM.currentSelectedRequest!.id); + requestVM.selectedDeliveryOptionEnum = null; + navigateWithName(context, AppRoutes.reviewRequestOffer, arguments: PaymentTypes.request); + }, ), ), ] diff --git a/lib/views/chat/widgets/chat_message_widget.dart b/lib/views/chat/widgets/chat_message_widget.dart index c2c7ca4..171bdb0 100644 --- a/lib/views/chat/widgets/chat_message_widget.dart +++ b/lib/views/chat/widgets/chat_message_widget.dart @@ -252,9 +252,18 @@ class _ChatMessageCustomWidgetState extends State { chatVM.serviceProviderOffersList[index].requestOfferStatusEnum = chatMessageModel.reqOffer!.requestOfferStatusEnum; } setState(() {}); - // Refresh requests list + + // Set loading state before refreshing + requestVM.updateIsRefreshingAfterAccept(true); + + // Refresh requests list to get updated data await requestVM.getRequestsBasedOnFilters(); + + // Clear loading state after refresh completes + requestVM.updateIsRefreshingAfterAccept(false); + Utils.showToast(LocaleKeys.offerAccepted.tr()); + return true; } else { return false; @@ -465,8 +474,16 @@ class _ChatMessageCustomWidgetState extends State { } setState(() {}); // Navigator.pop(context); - // Refresh requests list + + // Set loading state before refreshing + requestVM.updateIsRefreshingAfterAccept(true); + + // Refresh requests list to get updated data await requestVM.getRequestsBasedOnFilters(); + + // Clear loading state after refresh completes + requestVM.updateIsRefreshingAfterAccept(false); + Utils.showToast(LocaleKeys.offerAccepted.tr()); // navigateReplaceWithName(context, AppRoutes.dashboard); } diff --git a/lib/views/common_fragments/requests_fragment.dart b/lib/views/common_fragments/requests_fragment.dart index 8000122..ec45800 100644 --- a/lib/views/common_fragments/requests_fragment.dart +++ b/lib/views/common_fragments/requests_fragment.dart @@ -29,9 +29,7 @@ class MyRequestsFragment extends StatelessWidget { void cancelRequestBottomSheet(BuildContext context, int requestId) { return actionConfirmationBottomSheet( context: context, - title: LocaleKeys.cancelRequestPrompt - .tr() - .toText(fontSize: 28, isBold: true, letterSpacing: -1.44), + title: LocaleKeys.cancelRequestPrompt.tr().toText(fontSize: 28, isBold: true, letterSpacing: -1.44), subtitle: LocaleKeys.requestPermanentlyCancelled.tr(), actionButtonYes: Expanded( child: ShowFillButton( @@ -43,10 +41,7 @@ class MyRequestsFragment extends StatelessWidget { final requestVM = context.read(); - bool status = await requestVM.onActionRequestTapped( - context: context, - requestStatusEnum: RequestStatusEnum.cancelled, - requestId: requestId); + bool status = await requestVM.onActionRequestTapped(context: context, requestStatusEnum: RequestStatusEnum.cancelled, requestId: requestId); if (status) { await requestVM.getRequests(isNeedToRebuild: true); } @@ -74,8 +69,7 @@ class MyRequestsFragment extends StatelessWidget { return false; } - if (requestStatusEnum == RequestStatusEnum.pending || - requestStatusEnum == RequestStatusEnum.submitted) { + if (requestStatusEnum == RequestStatusEnum.pending || requestStatusEnum == RequestStatusEnum.submitted) { return true; } return false; @@ -83,8 +77,40 @@ class MyRequestsFragment extends StatelessWidget { @override Widget build(BuildContext context) { - return Consumer( - builder: (BuildContext context, RequestsVM requestsVM, Widget? child) { + // Access guard: Only dealerships can access Special Requests + if (AppState().userType != UserType.providerDealer && AppState().currentAppType == AppType.provider) { + return Scaffold( + appBar: CustomAppBar( + title: LocaleKeys.manageRequests.tr(), + isRemoveBackButton: true, + ), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.block, + size: 64, + color: MyColors.lightTextColor, + ), + 16.height, + LocaleKeys.accessDenied.tr().toText( + fontSize: 20, + isBold: true, + ), + 8.height, + "This feature is only available for Dealerships.".toText( + fontSize: 14, + color: MyColors.lightTextColor, + textAlign: TextAlign.center, + ), + ], + ).paddingAll(24), + ), + ); + } + + return Consumer(builder: (BuildContext context, RequestsVM requestsVM, Widget? child) { return Scaffold( appBar: CustomAppBar( title: LocaleKeys.manageRequests.tr(), @@ -93,9 +119,7 @@ class MyRequestsFragment extends StatelessWidget { if (AppState().currentAppType == AppType.provider) ...[ Padding( padding: const EdgeInsets.only(top: 8, bottom: 8, right: 15), - child: const Icon(Icons.messenger_outline_rounded, - color: Colors.black, size: 18) - .toContainer( + child: const Icon(Icons.messenger_outline_rounded, color: Colors.black, size: 18).toContainer( borderRadius: 80, borderColor: MyColors.lightGreyEFColor, isEnabledBorder: true, @@ -105,9 +129,7 @@ class MyRequestsFragment extends StatelessWidget { }) ], Padding( - padding: EdgeInsets.only( - top: requestsVM.requestsFiltersCounter > 0 ? 20 : 0, - right: 21), + padding: EdgeInsets.only(top: requestsVM.requestsFiltersCounter > 0 ? 20 : 0, right: 21), child: Badge( isLabelVisible: requestsVM.requestsFiltersCounter > 0, largeSize: 15, @@ -121,8 +143,7 @@ class MyRequestsFragment extends StatelessWidget { }), ], ).toViewOnly(context, onTap: () { - navigateWithName(context, AppRoutes.loginWithPassword, - arguments: false); + navigateWithName(context, AppRoutes.loginWithPassword, arguments: false); }), body: Container( color: MyColors.backgroundColor, @@ -134,8 +155,7 @@ class MyRequestsFragment extends StatelessWidget { FiltersList( filterList: requestsVM.requestsTypeFilterOptions, onFilterTapped: (index, selectedFilterId) { - requestsVM.applyFilterOnRequestsVM( - requestsTypeEnum: selectedFilterId.toRequestTypeEnum()); + requestsVM.applyFilterOnRequestsVM(requestsTypeEnum: selectedFilterId.toRequestTypeEnum()); }, ), // Show condition filters for all request types @@ -144,31 +164,27 @@ class MyRequestsFragment extends StatelessWidget { FiltersList( filterList: requestsVM.sparePartsConditionFilterOptions, onFilterTapped: (index, selectedFilterId) { - requestsVM.applySparePartsConditionFilter( - conditionId: selectedFilterId); + requestsVM.applySparePartsConditionFilter(conditionId: selectedFilterId); }, ), ], 8.height, Expanded( child: RefreshIndicator( - onRefresh: () async => - await requestsVM.getRequestsBasedOnFilters(), + onRefresh: () async => await requestsVM.getRequestsBasedOnFilters(), child: requestsVM.state == ViewState.busy ? const RequestsListShimmer(itemCount: 5) : requestsVM.myFilteredRequests.isEmpty ? Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - LocaleKeys.noRequeststoShow.tr().toText( - fontSize: 16, color: MyColors.lightTextColor), + LocaleKeys.noRequeststoShow.tr().toText(fontSize: 16, color: MyColors.lightTextColor), if (requestsVM.requestsFiltersCounter > 0) ...[ 8.height, InkWell( onTap: () async { requestsVM.clearRequestsFilters(); - await requestsVM - .getRequestsBasedOnFilters(); + await requestsVM.getRequestsBasedOnFilters(); }, child: LocaleKeys.clearFilters.tr().toText( fontSize: 14, @@ -187,76 +203,43 @@ class MyRequestsFragment extends StatelessWidget { secondaryBackground: Container( color: const Color(0xFFDD0505), child: Row( - crossAxisAlignment: - CrossAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.end, children: [ - LocaleKeys.cancel - .tr() - .toText( - fontSize: 15, - fontWeight: MyFonts.Medium, - height: 23 / 25, - color: MyColors.white) - .margin(right: 23), + LocaleKeys.cancel.tr().toText(fontSize: 15, fontWeight: MyFonts.Medium, height: 23 / 25, color: MyColors.white).margin(right: 23), ], ), ), dragStartBehavior: DragStartBehavior.start, - direction: shouldDismiss(requestsVM - .myFilteredRequests[index] - .requestStatus) - ? DismissDirection.endToStart - : DismissDirection.none, - confirmDismiss: - (DismissDirection direction) async { - cancelRequestBottomSheet(context, - requestsVM.myFilteredRequests[index].id); + direction: shouldDismiss(requestsVM.myFilteredRequests[index].requestStatus) ? DismissDirection.endToStart : DismissDirection.none, + confirmDismiss: (DismissDirection direction) async { + cancelRequestBottomSheet(context, requestsVM.myFilteredRequests[index].id); return; }, child: RequestItem( - request: - requestsVM.myFilteredRequests[index], + request: requestsVM.myFilteredRequests[index], shouldShowStatuses: true, onTap: () async { - RequestModel request = - requestsVM.myFilteredRequests[index]; - requestsVM.updateCurrentSelectedRequest( - request); + RequestModel request = requestsVM.myFilteredRequests[index]; + requestsVM.updateCurrentSelectedRequest(request); log("request.requestStatus: ${request.requestStatus}"); - if (request.requestStatus == - RequestStatusEnum.pending || - request.requestStatus == - RequestStatusEnum.cancelled || - request.requestStatus == - RequestStatusEnum.expired) { + if (request.requestStatus == RequestStatusEnum.pending || + request.requestStatus == RequestStatusEnum.cancelled || + request.requestStatus == RequestStatusEnum.expired) { return; } - if (AppState().currentAppType == - AppType.provider) { - RequestDetailPageArguments - requestDetailPageArguments = - RequestDetailPageArguments( - requestIndex: index, - requestModel: request); - navigateWithName(context, - AppRoutes.requestsDetailPage, - arguments: - requestDetailPageArguments); + if (AppState().currentAppType == AppType.provider) { + RequestDetailPageArguments requestDetailPageArguments = RequestDetailPageArguments(requestIndex: index, requestModel: request); + navigateWithName(context, AppRoutes.requestsDetailPage, arguments: requestDetailPageArguments); } else { - requestsVM.myFilteredRequests[index] - .offerCount = 0; + requestsVM.myFilteredRequests[index].offerCount = 0; requestsVM.notifyListeners(); - navigateWithName( - context, AppRoutes.offersListPage, - arguments: request.id); + navigateWithName(context, AppRoutes.offersListPage, arguments: request.id); } }).toViewOnly(context, onTap: () { - navigateWithName( - context, AppRoutes.loginWithPassword, - arguments: false); + navigateWithName(context, AppRoutes.loginWithPassword, arguments: false); }), ); }, @@ -264,8 +247,7 @@ class MyRequestsFragment extends StatelessWidget { return 16.height; }, itemCount: requestsVM.myFilteredRequests.length, - padding: const EdgeInsets.only( - left: 16, right: 16, bottom: 16, top: 8), + padding: const EdgeInsets.only(left: 16, right: 16, bottom: 16, top: 8), ), )) ], @@ -273,16 +255,14 @@ class MyRequestsFragment extends StatelessWidget { ), floatingActionButton: AppState().currentAppType == AppType.customer ? FloatingActionButton( - onPressed: () => - navigateWithName(context, AppRoutes.createRequestPage), + onPressed: () => navigateWithName(context, AppRoutes.createRequestPage), backgroundColor: MyColors.darkPrimaryColor, child: const Icon( Icons.add, color: MyColors.white, ), ).toViewOnly(context, onTap: () { - navigateWithName(context, AppRoutes.loginWithPassword, - arguments: false); + navigateWithName(context, AppRoutes.loginWithPassword, arguments: false); }) : null, ); diff --git a/lib/views/profile/profile_view.dart b/lib/views/profile/profile_view.dart index 8fbbc7d..e172a02 100644 --- a/lib/views/profile/profile_view.dart +++ b/lib/views/profile/profile_view.dart @@ -5,6 +5,7 @@ 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/config/routes.dart'; +import 'package:mc_common_app/extensions/enum_extensions.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'; @@ -127,6 +128,17 @@ class _ProfileScreenState extends State { onTap: () {}, ), ], + if (AppState().currentAppType == AppType.provider) ...[ + CustomProfileOptionsTile( + titleText: LocaleKeys.userType.tr(), + subtitleText: AppState().userType.toDisplayName(), + needBorderBelow: true, + needEditButton: false, + onTap: () { + // User type is read-only + }, + ), + ], CustomProfileOptionsTile( titleText: LocaleKeys.city.tr(), subtitleText: "${AppState().getUser.data!.userInfo!.cityName ?? ""}, ${AppState().getUser.data!.userInfo!.countryName ?? ""}", diff --git a/lib/views/requests/widget/request_item.dart b/lib/views/requests/widget/request_item.dart index 540d1c8..e03743d 100644 --- a/lib/views/requests/widget/request_item.dart +++ b/lib/views/requests/widget/request_item.dart @@ -32,8 +32,8 @@ class RequestItem extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.start, children: [ imageUrl.buildNetworkImage( - width: 80, - height: 80, + width: 60, + height: 60, fit: BoxFit.cover, ), 12.width, @@ -63,6 +63,8 @@ class RequestItem extends StatelessWidget { if (request.vehicleTypeName.isNotEmpty) ...[ showItem("${LocaleKeys.vehicleType.tr()}:", request.vehicleTypeName), ], + // Show condition (New/Used) + showItem("${LocaleKeys.condition.tr()}:", request.isNew ? LocaleKeys.newCondition.tr() : LocaleKeys.used.tr()), showItem("${LocaleKeys.description.tr()}:", request.description), ], ), diff --git a/lib/views/setting_options/setting_options_more.dart b/lib/views/setting_options/setting_options_more.dart index 62483dd..42731e4 100644 --- a/lib/views/setting_options/setting_options_more.dart +++ b/lib/views/setting_options/setting_options_more.dart @@ -12,7 +12,6 @@ import 'package:mc_common_app/theme/colors.dart'; import 'package:mc_common_app/utils/dialogs_and_bottomsheets.dart'; import 'package:mc_common_app/utils/enums.dart'; import 'package:mc_common_app/utils/navigator.dart'; -import 'package:mc_common_app/view_models/setting_options_view_model.dart'; import 'package:mc_common_app/view_models/user_view_model.dart'; import 'package:mc_common_app/views/setting_options/widgets/custom_setting_options_tile.dart'; import 'package:mc_common_app/widgets/button/show_fill_button.dart'; diff --git a/lib/widgets/button/show_fill_button.dart b/lib/widgets/button/show_fill_button.dart index 54dc7db..fdc6b8f 100644 --- a/lib/widgets/button/show_fill_button.dart +++ b/lib/widgets/button/show_fill_button.dart @@ -7,13 +7,14 @@ import 'package:mc_common_app/classes/consts.dart'; class ShowFillButton extends StatelessWidget { final String title; final Color? backgroundColor; - final VoidCallback onPressed; + final VoidCallback? onPressed; final Color txtColor; final double elevation, radius, maxWidth, maxHeight, fontSize, horizontalPadding, horizontalMargin, verticalMargin; final bool isFlatButton, isBold; final EdgeInsets? margin; final bool isFilled; final bool isDisabled; + final bool isLoading; final Color borderColor; final Widget? iconWidget; @@ -26,6 +27,7 @@ class ShowFillButton extends StatelessWidget { this.elevation = 0, this.isFilled = true, this.isDisabled = false, + this.isLoading = false, this.radius = 0, this.maxWidth = 88, this.maxHeight = 55, @@ -68,7 +70,7 @@ class ShowFillButton extends StatelessWidget { Widget showButton() { return Container( // decoration: isFlatButton ? null : MyColors.gradientButton, - color: isDisabled + color: (isDisabled || isLoading) ? MyColors.grey98Color.withOpacity(0.3) : (isFlatButton ? null @@ -77,32 +79,41 @@ class ShowFillButton extends StatelessWidget { : null), margin: EdgeInsets.symmetric(horizontal: horizontalMargin, vertical: verticalMargin), child: MaterialButton( - onPressed: onPressed, + onPressed: (isLoading || onPressed == null) ? null : onPressed, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(radius), side: isFilled ? BorderSide.none : BorderSide(width: 2, color: borderColor), ), - child: iconWidget != null - ? Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - iconWidget!, - title.toText( + child: isLoading + ? SizedBox( + height: 24, + width: 24, + child: CircularProgressIndicator( + strokeWidth: 2.5, + valueColor: AlwaysStoppedAnimation(txtColor), + ), + ) + : iconWidget != null + ? Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + iconWidget!, + title.toText( + fontSize: fontSize, + isBold: isBold, + fontWeight: MyFonts.Medium, + color: isDisabled ? MyColors.lightTextColor : txtColor, + maxLines: 1, + ), + ], + ) + : title.toText( fontSize: fontSize, isBold: isBold, fontWeight: MyFonts.Medium, color: isDisabled ? MyColors.lightTextColor : txtColor, maxLines: 1, ), - ], - ) - : title.toText( - fontSize: fontSize, - isBold: isBold, - fontWeight: MyFonts.Medium, - color: isDisabled ? MyColors.lightTextColor : txtColor, - maxLines: 1, - ), ), ); } diff --git a/lib/widgets/common_widgets/bottom_nav_bar.dart b/lib/widgets/common_widgets/bottom_nav_bar.dart index 2f78905..f009ff4 100644 --- a/lib/widgets/common_widgets/bottom_nav_bar.dart +++ b/lib/widgets/common_widgets/bottom_nav_bar.dart @@ -1,9 +1,11 @@ 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/generated/locale_keys.g.dart'; import 'package:mc_common_app/theme/colors.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:mc_common_app/classes/consts.dart'; +import 'package:mc_common_app/utils/enums.dart'; import 'package:mc_common_app/widgets/extensions/extensions_widget.dart'; class CustomBottomNavbar extends StatelessWidget { @@ -47,7 +49,8 @@ class CustomBottomNavbar extends StatelessWidget { activeIcon: SvgPicture.asset(MyAssets.announcementIcon, color: MyColors.darkIconColor).paddingAll(5), label: LocaleKeys.ads.tr(), ), - if (isProvider) + // Only show Requests tab for Dealerships (not for Individual Providers) + if (isProvider && AppState().userType == UserType.providerDealer) BottomNavigationBarItem( icon: SvgPicture.asset(MyAssets.icRequests).paddingAll(5), activeIcon: SvgPicture.asset(MyAssets.icRequests, color: MyColors.darkIconColor).paddingAll(5), diff --git a/lib/widgets/common_widgets/filters_list.dart b/lib/widgets/common_widgets/filters_list.dart index 19231cf..63a1724 100644 --- a/lib/widgets/common_widgets/filters_list.dart +++ b/lib/widgets/common_widgets/filters_list.dart @@ -2,19 +2,19 @@ import 'dart:developer'; 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/extensions/string_extensions.dart'; import 'package:mc_common_app/models/general_models/widgets_models.dart'; import 'package:mc_common_app/theme/colors.dart'; import 'package:mc_common_app/widgets/extensions/extensions_widget.dart'; -import '../../classes/app_state.dart'; - class FiltersList extends StatelessWidget { final List filterList; final Function(int, int) onFilterTapped; final bool needLeftPadding; final bool showImages; final EdgeInsets? padding; + final ScrollController? scrollController; const FiltersList({ super.key, @@ -23,6 +23,7 @@ class FiltersList extends StatelessWidget { this.needLeftPadding = true, this.showImages = false, required this.onFilterTapped, + this.scrollController, }); @override @@ -32,6 +33,7 @@ class FiltersList extends StatelessWidget { height: 37, width: double.infinity, child: ListView.builder( + controller: scrollController, padding: padding ?? EdgeInsets.only(left: needLeftPadding ? 12 : 0), itemCount: filterList.length, scrollDirection: Axis.horizontal,