diff --git a/assets/langs/ar-SA.json b/assets/langs/ar-SA.json index d011361..24c4210 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": "دردشة طلب السيارة الخاصة", @@ -906,6 +912,9 @@ "iosApp": "آيفون", "pleaseWait": "يرجى الانتظار...", "numberOfShipping": "عدد الشحنات", + "loading": "جار التحميل...", + "pleaseSelectAccidentFree": "يرجى تحديد ما إذا كانت المركبة خالية من الحوادث", + "pleaseSelectMileageIncrease": "يرجى تحديد ما إذا كان يمكن زيادة المسافة المقطوعة", "formatNeedsUpdate": "تنسيق الاعلان يحتاج الى تحديث", "noAdsMatchesYourSearch" :"لا توجد اعلانات تطابق بحثك" } \ No newline at end of file diff --git a/assets/langs/en-US.json b/assets/langs/en-US.json index e057dfd..a5bac7e 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/api/api_client.dart b/lib/api/api_client.dart index 5d812b8..54aeebb 100644 --- a/lib/api/api_client.dart +++ b/lib/api/api_client.dart @@ -124,17 +124,16 @@ class ApiClientImp implements ApiClient { return await _postForResponse(url, requestBody, token: token, queryParameters: queryParameters, headers: headers, retryTimes: retryTimes); } - Future _postForResponse( - String url, - requestBody, { - String? token, - Map? queryParameters, - Map? headers, - int retryTimes = 0, - }) async { + Future _postForResponse(String url, + requestBody, { + String? token, + Map? queryParameters, + Map? headers, + int retryTimes = 0, + }) async { try { var headers0 = {}; - + // Add Accept-Language header based on current locale headers0['Accept-Language'] = _getCurrentLanguage(); @@ -211,7 +210,8 @@ class ApiClientImp implements ApiClient { bool _certificateCheck(X509Certificate cert, String host, int port) => true; Future _withClient(Future Function(Client) fn) async { - var httpClient = HttpClient()..badCertificateCallback = _certificateCheck; + var httpClient = HttpClient() + ..badCertificateCallback = _certificateCheck; var client = IOClient(httpClient); try { return await fn(client); @@ -254,7 +254,7 @@ class ApiClientImp implements ApiClient { Future _getForResponse(String url, {String? token, Map? queryParameters, Map? headers, int retryTimes = 0}) async { try { var headers0 = {}; - + // Add Accept-Language header based on current locale headers0['Accept-Language'] = _getCurrentLanguage(); diff --git a/lib/extensions/string_extensions.dart b/lib/extensions/string_extensions.dart index 5981f10..eb992d5 100644 --- a/lib/extensions/string_extensions.dart +++ b/lib/extensions/string_extensions.dart @@ -597,6 +597,8 @@ extension AdReserveStatusEnum on int { } else if (this == 5) { return AdReserveStatus.dealDone; } else if (this == 6) { + return AdReserveStatus.paymentDoneByCustomer; + } else if (this == 7) { return AdReserveStatus.fullPaymentVerified; } else { return AdReserveStatus.defaultStatus; @@ -734,9 +736,7 @@ extension DateTimeConversions on DateTime { return numericDates ? '1 week ago' : 'Last week'; } else if (difference.inDays >= 2) { - return isArabic - ? 'منذ ${difference.inDays} أيام' - : '${difference.inDays} days ago'; + return isArabic ? 'منذ ${difference.inDays} أيام' : '${difference.inDays} days ago'; } else if (difference.inDays >= 1) { if (isArabic) { return numericDates ? 'منذ يوم' : 'أمس'; @@ -744,9 +744,7 @@ extension DateTimeConversions on DateTime { return numericDates ? '1 day ago' : 'Yesterday'; } else if (difference.inHours >= 2) { - return isArabic - ? 'منذ ${difference.inHours} ساعات' - : '${difference.inHours} hours ago'; + return isArabic ? 'منذ ${difference.inHours} ساعات' : '${difference.inHours} hours ago'; } else if (difference.inHours >= 1) { if (isArabic) { return numericDates ? 'منذ ساعة' : 'قبل ساعة'; @@ -754,9 +752,7 @@ extension DateTimeConversions on DateTime { return numericDates ? '1 hour ago' : 'An hour ago'; } else if (difference.inMinutes >= 2) { - return isArabic - ? 'منذ ${difference.inMinutes} دقائق' - : '${difference.inMinutes} min ago'; + return isArabic ? 'منذ ${difference.inMinutes} دقائق' : '${difference.inMinutes} min ago'; } else if (difference.inMinutes >= 1) { if (isArabic) { return numericDates ? 'منذ دقيقة' : 'قبل دقيقة'; @@ -764,9 +760,7 @@ extension DateTimeConversions on DateTime { return numericDates ? '1 min ago' : 'A min ago'; } else if (difference.inSeconds >= 3) { - return isArabic - ? 'منذ ${difference.inSeconds} ثوانٍ' - : '${difference.inSeconds} sec ago'; + return isArabic ? 'منذ ${difference.inSeconds} ثوانٍ' : '${difference.inSeconds} sec ago'; } else { return isArabic ? 'الآن' : 'Just now'; } @@ -1227,3 +1221,19 @@ extension AdCreationStepsFromInt on int { return AdCreationStepsEnum.vehicleDetails; } } + +extension UserTypeExtension on UserType { + /// Returns a user-friendly display name for the UserType + String toDisplayName() { + switch (this) { + case UserType.providerDealer: + return "Provider - Dealership"; + case UserType.providerIndividual: + return "Provider - Individual"; + case UserType.customer: + return "Customer"; + default: + return toString(); // Fallback to default toString + } + } +} 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 712f754..1cfaa41 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,6 +893,10 @@ 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'; static const formatNeedsUpdate = 'formatNeedsUpdate'; static const noAdsMatchesYourSearch = 'noAdsMatchesYourSearch'; + } diff --git a/lib/models/advertisment_models/ad_details_model.dart b/lib/models/advertisment_models/ad_details_model.dart index fc0f612..2f43ffa 100644 --- a/lib/models/advertisment_models/ad_details_model.dart +++ b/lib/models/advertisment_models/ad_details_model.dart @@ -44,9 +44,13 @@ class AdDetailsModel { String? modifiedOn; AdPostStatus? adPostStatus; AdReserveStatus? adReserveStatus; + 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; @@ -94,9 +98,13 @@ class AdDetailsModel { this.isOnWhatsApp, this.adPostStatus, this.adReserveStatus, + this.adsReserveId, + this.isReservable, + this.reserveCustomerMessage, this.isMyAd, this.isReservedByMe, this.isAdFavorite, + this.favouriteAdID, this.phoneNo, this.whatsAppNo, this.adOwnerName, @@ -163,10 +171,13 @@ class AdDetailsModel { adOwnerDetails = (json['vehicle'] != null && json['vehicle']['aDsUser'] != null) ? (AdOwnerDetails.fromJson(json['vehicle']['aDsUser'])) : null; adPostStatus = ((json['statusID'] ?? 1) as int).toAdPostEnum(); adReserveStatus = AdReserveStatus.defaultStatus; + isReservable = json['isReservable'] ?? false; + reserveCustomerMessage = json['reserveCustomerMessage']; createdByRoleEnum = (json['createdByRole'] as int).toCreatedByRoleEnum(); 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/repositories/ads_repo.dart b/lib/repositories/ads_repo.dart index 8a2a12f..7bd6964 100644 --- a/lib/repositories/ads_repo.dart +++ b/lib/repositories/ads_repo.dart @@ -74,7 +74,7 @@ abstract class AdsRepo { Future deleteAd({required int adId}); - Future cancelMyAdReservation({required int adId, required int adsReserveStatus, required String reason}); + Future cancelMyAdReservation({required int adId, required int adsReserveId, required int adsReserveStatus, required String reason}); Future createReserveAd({required int adId}); @@ -451,6 +451,7 @@ class AdsRepoImp implements AdsRepo { Future> getMyReservedAds() async { var params = { "userID": appState.getUser.data!.userInfo!.userId ?? "", + "IsAdCurrentlyReserved": "true", }; GenericRespModel adsGenericModel = await apiClient.getJsonForObject( @@ -493,26 +494,29 @@ class AdsRepoImp implements AdsRepo { List vehicleAdsDetails = List.generate(adsGenericModel.data.length, (index) => AdDetailsModel.fromJson(adsGenericModel.data[index], true, adsGenericModel.totalItemsCount ?? 1)); - // Create a map for O(1) lookup: adsID -> reserve status + // Create maps for O(1) lookup: adsID -> reserve status and reserve ID Map statusMap = {}; + Map reserveIdMap = {}; // adsID -> reserve ID for (var reserved in reservedAds) { log("adsReserveStatus: ${reserved.adsReserveStatus}"); statusMap[reserved.adsID ?? 0] = (reserved.adsReserveStatus ?? 0).toAdRserveStatusEnum(); + reserveIdMap[reserved.adsID ?? 0] = reserved.id ?? 0; // Store reserve ID } log("📋 Status Map created: ${statusMap.length} entries"); statusMap.forEach((key, value) { - log(" Ad ID: $key -> Status: $value"); + log(" Ad ID: $key -> Status: $value, Reserve ID: ${reserveIdMap[key]}"); }); - // Assign reserve status by matching ad ID (not by index!) + // Assign reserve status and reserve ID by matching ad ID (not by index!) log("📦 Processing ${vehicleAdsDetails.length} ads from second API"); for (var ad in vehicleAdsDetails) { if (statusMap.containsKey(ad.id)) { ad.adReserveStatus = statusMap[ad.id]!; + ad.adsReserveId = reserveIdMap[ad.id]!; // Store reserve ID // Backend will set adPostStatus=reserved in the response, so no need to set it manually - log(" ✅ Ad ${ad.id}: Assigned adReserveStatus ${ad.adReserveStatus}"); + log(" ✅ Ad ${ad.id}: Assigned adReserveStatus ${ad.adReserveStatus}, adsReserveId ${ad.adsReserveId}"); } else { // Ad exists but no reserve status found - shouldn't happen but handle it ad.adReserveStatus = AdReserveStatus.defaultStatus; @@ -690,10 +694,11 @@ class AdsRepoImp implements AdsRepo { } @override - Future cancelMyAdReservation({required int adId, required int adsReserveStatus, required String reason}) async { + Future cancelMyAdReservation({required int adId, required int adsReserveId, required int adsReserveStatus, required String reason}) async { int customerID = AppState().getUser.data!.userInfo!.customerId ?? 0; var postParams = { + "ID": adsReserveId, // Reserve ID (mandatory as per backend requirement) "adsID": adId, "adsReserveStatus": adsReserveStatus, "comment": reason, @@ -794,7 +799,7 @@ class AdsRepoImp implements AdsRepo { String token = appState.getUser.data!.accessToken ?? ""; GenericRespModel adsGenericModel = await apiClient.postJsonForObject( (json) => GenericRespModel.fromJson(json), - "", + ApiConsts.reserveAdPaymentOnDealDoneCreate, postParams, token: token, ); diff --git a/lib/services/firebase_service.dart b/lib/services/firebase_service.dart index 63fb768..5a16329 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,24 @@ 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( @@ -120,6 +140,7 @@ class FirebaseMessagingServiceImp implements FirebaseMessagingService { log("Data payload: ${message.data}"); log("Message ID: ${message.messageId}"); log("Sent time: ${message.sentTime}"); + log("Android config: ${message.toMap()}"); log("======================================================"); // Just display notification - DON'T navigate automatically @@ -171,7 +192,7 @@ class FirebaseMessagingServiceImp implements FirebaseMessagingService { // Backend sends NotiType as string: "1" = Request, "2" = Ads, "3" = General String? notiTypeStr = data['NotiType']; int? notiType = int.tryParse(notiTypeStr ?? ''); - + log("NotiType from backend: $notiTypeStr (parsed: $notiType)"); if (notiType == null || notiType < 1 || notiType > 3) { @@ -243,7 +264,7 @@ class FirebaseMessagingServiceImp implements FirebaseMessagingService { try { // Try both backend format and our format int requestId = int.tryParse(data['RequestID']?.toString() ?? data['request_id']?.toString() ?? '0') ?? 0; - + if (requestId == 0) { log("❌ Invalid or missing RequestID in notification data"); return; @@ -252,7 +273,7 @@ class FirebaseMessagingServiceImp implements FirebaseMessagingService { log("✅ Navigating to providers chat list for request: $requestId"); navigateWithName(context, AppRoutes.providersChatListPage); - // TODO: If backend sends all required data (ProviderId, CustomerId), + // TODO: If backend sends all required data (ProviderId, CustomerId), // we can navigate directly to the chat screen } catch (e) { logger.e("Error navigating to request chat: $e"); @@ -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( @@ -293,7 +327,7 @@ class FirebaseMessagingServiceImp implements FirebaseMessagingService { try { // Try both backend format and our format int requestId = int.tryParse(data['RequestID']?.toString() ?? data['request_id']?.toString() ?? '0') ?? 0; - + if (requestId == 0) { log("❌ Invalid or missing RequestID in notification data"); return; @@ -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 f386e12..b566d73 100644 --- a/lib/utils/enums.dart +++ b/lib/utils/enums.dart @@ -5,10 +5,6 @@ // unverified, // } -// - -// - class AppEnums { static const int requestFilterEnumId = 16; // Requests Filter Enums static const int branchServicesEnumId = 6; // Branch Services Filter Enums @@ -27,6 +23,8 @@ enum DashboardRouteEnum { fromAdsPayment, fromAdsSubmit, fromReserveAdsPayment, + fromAppointmentPayment, + fromRequestPayment, none, } @@ -44,6 +42,7 @@ enum AdReserveStatus { cancelledByAdmin, timeOver, dealDone, + paymentDoneByCustomer, fullPaymentVerified, } diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart index 0ea0b16..de2a614 100644 --- a/lib/utils/utils.dart +++ b/lib/utils/utils.dart @@ -276,6 +276,11 @@ class Utils { return MyColors.adCancelledStatusColor; // Cancelled - red case AdReserveStatus.dealDone: + return MyColors.textColor; // pending - gray + + case AdReserveStatus.paymentDoneByCustomer: + return MyColors.textColor; // paymentDone - green + case AdReserveStatus.fullPaymentVerified: return MyColors.greenColor; // Completed successfully - green @@ -299,8 +304,11 @@ class Utils { case AdReserveStatus.cancelledByAdmin: return LocaleKeys.cancelledByAdmin.tr(); - case AdReserveStatus.dealDone: - return LocaleKeys.dealCompleted.tr(); + case AdReserveStatus.dealDone: // means pending complete payment + return LocaleKeys.pendingForPayment.tr(); + + case AdReserveStatus.paymentDoneByCustomer: + return LocaleKeys.waitingAdminsResponse.tr(); case AdReserveStatus.fullPaymentVerified: return LocaleKeys.paymentVerified.tr(); diff --git a/lib/view_models/ad_view_model.dart b/lib/view_models/ad_view_model.dart index 0c72f25..5ba14cb 100644 --- a/lib/view_models/ad_view_model.dart +++ b/lib/view_models/ad_view_model.dart @@ -14,6 +14,7 @@ import 'package:mc_common_app/main.dart'; import 'package:mc_common_app/models/advertisment_models/ad_details_model.dart'; import 'package:mc_common_app/models/advertisment_models/ads_bank_details_model.dart'; import 'package:mc_common_app/models/advertisment_models/ads_duration_model.dart'; +import 'package:mc_common_app/models/advertisment_models/pdf_receipt_model.dart'; import 'package:mc_common_app/models/advertisment_models/special_service_model.dart'; import 'package:mc_common_app/models/advertisment_models/ss_car_check_schedule_model.dart'; import 'package:mc_common_app/models/advertisment_models/ss_photo_schedule_model.dart'; @@ -81,6 +82,7 @@ class AdVM extends BaseVM { String damagePartSearchValue = ""; String adReservePriceError = ""; String adPhoneNumberError = ""; + String accidentFreeError = ""; // Edit Variables Amir bool isExtendAdEditEnabled = false; @@ -523,7 +525,7 @@ class AdVM extends BaseVM { Future getAdBankingAccountInfo({required int adId}) async { setState(ViewState.busy); try { - adsBankDetailsModel = await adsRepo.getAdBankingAccountInfo(adId: 21); + adsBankDetailsModel = await adsRepo.getAdBankingAccountInfo(adId: adId); setState(ViewState.idle); } catch (e) { setState(ViewState.idle); @@ -563,20 +565,20 @@ class AdVM extends BaseVM { navigateReplaceWithName(context, AppRoutes.dashboard, arguments: DashboardRouteEnum.fromAdsSubmit); } - Future cancelMyAdReservation(BuildContext context, {required int adId, required String reason}) async { + Future cancelMyAdReservation(BuildContext context, {required int adId, required int adsReserveId, required String reason}) async { Utils.showLoading(context); - GenericRespModel respModel = await adsRepo.cancelMyAdReservation(adId: adId, adsReserveStatus: 11, reason: reason); // 11 is to Cancel Reservation + GenericRespModel respModel = await adsRepo.cancelMyAdReservation(adId: adId, adsReserveId: adsReserveId, adsReserveStatus: 2, reason: reason); // 2 for AdsReserveStatus is to Cancel Reservation if (respModel.messageStatus != 1) { Utils.hideLoading(context); Utils.showToast(respModel.message ?? LocaleKeys.somethingWrong.tr()); return; } - + // Reset the cancel reason text field reservationCancelReason = ""; reservationCancelError = ""; - + Utils.hideLoading(context); Utils.showToast(LocaleKeys.yourReservationCancelled.tr()); updateIsExploreAds(false); @@ -820,10 +822,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 +837,7 @@ class AdVM extends BaseVM { void updateIsAccidentFree(bool value) { isAccidentFree = value; isAccidentFreeSet = true; + accidentFreeError = ""; // Clear error when user makes a selection notifyListeners(); } @@ -1228,6 +1233,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; @@ -1774,31 +1796,81 @@ class AdVM extends BaseVM { notifyListeners(); } - List pickedReceiptPdfFiles = []; + // Single receipt file for Complete Deal flow + ImageModel? pickedReceiptFile; - String receiptPdfFileError = ""; + String receiptFileError = ""; - void removePdfFileFromList(String filePath) { - int index = pickedReceiptPdfFiles.indexWhere((element) => element.filePath == filePath); - if (index == -1) { - return; - } - pickedReceiptPdfFiles.removeAt(index); + void removeReceiptFile() { + pickedReceiptFile = null; notifyListeners(); } - void pickPdfReceiptFile(BuildContext context) async { - List imageModels = []; - List? files = await commonServices.pickMultipleFiles(context); - if (files == null) return; - for (var element in files) { - imageModels.add(ImageModel(filePath: element.path, isFromNetwork: false)); - } - pickedReceiptPdfFiles.addAll(imageModels); - if (pickedReceiptPdfFiles.isNotEmpty) receiptPdfFileError = ""; + void pickReceiptFile(BuildContext context) async { + List? files = await commonServices.pickMultipleFiles(context, allowMultiple: false); + if (files == null || files.isEmpty) return; + + pickedReceiptFile = ImageModel(filePath: files.first.path, isFromNetwork: false); + if (pickedReceiptFile != null) receiptFileError = ""; notifyListeners(); } + // Upload bank receipt for Complete Deal + Future uploadBankReceipt(BuildContext context, {required int adId}) async { + if (pickedReceiptFile == null) { + receiptFileError = LocaleKeys.selectAttachment.tr(); + notifyListeners(); + return; + } + + try { + Utils.showLoading(context); + + // Convert file to base64 + File file = File(pickedReceiptFile!.filePath!); + List fileBytes = await file.readAsBytes(); + String base64File = base64Encode(fileBytes); + String fileName = file.path.split('/').last; + + // Create PdfReceiptModel for API + PdfReceiptModel pdfReceipt = PdfReceiptModel( + id: 0, + pdfName: fileName, + pdfString: base64File, + ); + + // Call repository method + GenericRespModel response = await adsRepo.uploadBankReceiptsOnReserveDealDone( + adId: adId, + detailNote: completeDealNotesForAdmin, + pdfReceiptsList: [pdfReceipt], + ); + + Utils.hideLoading(context); + + if (response.messageStatus == 1) { + Utils.showToast(response.message ?? "Receipt uploaded successfully"); + + // Reset values + pickedReceiptFile = null; + completeDealNotesForAdmin = ""; + receiptFileError = ""; + notifyListeners(); + + // Close bottom sheet + if (context.mounted) { + Navigator.of(context).pop(); + } + } else { + Utils.showToast(response.message ?? "Failed to upload receipt"); + } + } catch (e) { + Utils.hideLoading(context); + Utils.showToast("Error uploading receipt: $e"); + log("Error in uploadBankReceipt: $e"); + } + } + // sourceFlag for Camera = 0 // sourceFlag for Gallery = 1 void pickDamagePartImage(int index) async { @@ -1883,6 +1955,9 @@ class AdVM extends BaseVM { vehicleDamageCards.clear(); specialServiceCards.clear(); vehicleAdsSpecialServices.clear(); + pickedReceiptFile = null; + completeDealNotesForAdmin = ""; + receiptFileError = ""; currentProgressStep = AdCreationStepsEnum.vehicleDetails; vehicleTypeId = SelectionModel(selectedOption: "", selectedId: -1, errorValue: ""); vehicleModelId = SelectionModel(selectedOption: "", selectedId: -1, errorValue: ""); @@ -1908,9 +1983,11 @@ class AdVM extends BaseVM { adPhoneNumberDialCode = ""; adPhoneNumber = ""; odometer = ""; - isInUsed = 0; + isInUsed = null; + isInUsedError = ""; isAccidentFree = false; isAccidentFreeSet = false; + accidentFreeError = ""; clearSpecialServiceCard(); updateFinanceAvailableStatus(false); notifyListeners(); @@ -2102,7 +2179,7 @@ class AdVM extends BaseVM { } VehiclePosting vehiclePosting = VehiclePosting( - id: isAdEditEnabled ? previousAdDetails!.vehiclePostingID : null, + id: isExtendAdEditEnabled ? null : (isAdEditEnabled ? previousAdDetails!.vehiclePostingID : null), userID: appState.getUser.data!.userInfo!.userId, vehicleType: vehicleTypeId.selectedId, vehicleModelID: vehicleModelId.selectedId, @@ -2571,6 +2648,11 @@ class AdVM extends BaseVM { setState(ViewState.busy); } + // Auto-add current typed Ad ID to history if not empty and not already in list + if (currentAdIDFilter.isNotEmpty && !ifAlreadyExistForStringValue(list: adsFilterAdIDSearchHistory, value: currentAdIDFilter)) { + addAdFilterAdIDSearchHistory(value: currentAdIDFilter); + } + List adIdsList = []; if (adsFilterAdIDSearchHistory.isNotEmpty) { for (var element in adsFilterAdIDSearchHistory) { @@ -2624,6 +2706,11 @@ class AdVM extends BaseVM { } } + // Auto-add current typed phone number to history if not empty and not already in list + if (currentAdMobilePhoneFilter.isNotEmpty && !ifAlreadyExistForStringValue(list: adFilterMobilePhoneSearchHistory, value: currentAdMobilePhoneFilter)) { + addAdFilterMobilePhoneSearchHistory(value: currentAdMobilePhoneFilter); + } + List phoneNumbersList = []; if (adFilterMobilePhoneSearchHistory.isNotEmpty) { for (var element in adFilterMobilePhoneSearchHistory) { @@ -2677,7 +2764,7 @@ class AdVM extends BaseVM { void onEditUpdateAdPressed({required BuildContext context, required AdDetailsModel previousDetails, required bool isFromExtendAd, bool isForDraft = false}) { isAdEditEnabled = true; - isDraftEditEnabled = true; + isDraftEditEnabled = isForDraft; // Only true if editing a draft isExtendAdEditEnabled = isFromExtendAd; previousAdDetails = previousDetails; autoFillSelectedVehicleType(); 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 f465923..b0c7908 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(); } @@ -107,26 +107,61 @@ class DashboardVmCustomer extends BaseVM with WidgetsBindingObserver { RequestsVM requestsVM = Provider.of(context, listen: false); ChatVM chatVM = Provider.of(context, listen: false); + // Set loading state immediately to prevent "No data" flash + adVM.setState(ViewState.busy); + appointmentsVM.setState(ViewState.busy); + requestsVM.setState(ViewState.busy); + // ✅ Inject ViewModels into ChatVM (only needs to be done once, but safe to repeat) chatVM.setViewModels(requestsVM: requestsVM, adVM: adVM); - await appointmentsVM.populateBranchesFilterList(); - await appointmentsVM.populateAppointmentsFilterList(); - await adVM.populateAdsFilterList(); - await requestsVM.populateDataForRequestsFilter(); - await requestsVM.populateDeliveryOptionEnums(); - await appointmentsVM.applyFilterOnAppointmentsVMForCustomers(appointmentStatusEnum: AppointmentStatusEnum.allAppointments, shouldPopulateUpcoming: true); - await appointmentsVM.applyFilterOnBranches(index: 0); // to get all branches! - await appointmentsVM.getMyRecentBranches(); // to get my recent branches - - if (dashboardRouteEnum != DashboardRouteEnum.fromAdsPayment && dashboardRouteEnum != DashboardRouteEnum.fromAdsSubmit) { - await adVM.getMyAds(); - await adVM.getExploreAds(); + // Call independent filter population APIs in parallel + await Future.wait([ + appointmentsVM.populateBranchesFilterList(), + appointmentsVM.populateAppointmentsFilterList(), + adVM.populateAdsFilterList(), + requestsVM.populateDataForRequestsFilter(), + requestsVM.populateDeliveryOptionEnums(), + adVM.getVehicleTypes(), + adVM.getVehicleAdsDuration(), + ]); + + // Call data APIs in parallel + await Future.wait([ + appointmentsVM.applyFilterOnAppointmentsVMForCustomers(appointmentStatusEnum: AppointmentStatusEnum.allAppointments, shouldPopulateUpcoming: true), + appointmentsVM.applyFilterOnBranches(index: 0), // to get all branches! + appointmentsVM.getMyRecentBranches(), // to get my recent branches + requestsVM.getRequests(), + ]); + + // Call ads APIs in parallel (conditionally) + if (dashboardRouteEnum != DashboardRouteEnum.fromAdsPayment && dashboardRouteEnum != DashboardRouteEnum.fromAdsSubmit && dashboardRouteEnum != DashboardRouteEnum.fromReserveAdsPayment) { + await Future.wait([ + adVM.getMyAds(), + adVM.getExploreAds(), + ]); + } + + // Handle different payment completion scenarios + if (dashboardRouteEnum == DashboardRouteEnum.fromReserveAdsPayment) { + adVM.updateIsExploreAds(false); + 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(); - await adVM.getVehicleTypes(); - await adVM.getVehicleAdsDuration(); + // Build chat hub connection last await chatVM.buildHubConnection(context: context); // if (appointmentsVM.myAppointments.isEmpty) { diff --git a/lib/view_models/dashboard_view_model_provider.dart b/lib/view_models/dashboard_view_model_provider.dart index 9da5397..f822b13 100644 --- a/lib/view_models/dashboard_view_model_provider.dart +++ b/lib/view_models/dashboard_view_model_provider.dart @@ -1,3 +1,4 @@ +import 'dart:developer'; import 'dart:io'; import 'package:flutter/widgets.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; @@ -51,25 +52,25 @@ class DashboardVMProvider extends BaseVM with WidgetsBindingObserver { void didChangeAppLifecycleState(AppLifecycleState state) { super.didChangeAppLifecycleState(state); logger.i("App lifecycle state changed to: $state"); - + if (_context == null) return; - + final chatVM = Provider.of(_context!, listen: false); - + 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; } } @@ -79,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(); @@ -122,33 +130,58 @@ class DashboardVMProvider extends BaseVM with WidgetsBindingObserver { final serviceVM = context.read(); final adVM = context.read(); final subscriptionsVM = context.read(); - + + // Set loading state immediately to prevent "No data" flash + adVM.setState(ViewState.busy); + appointmentVM.setState(ViewState.busy); + requestsVM.setState(ViewState.busy); + serviceVM.setState(ViewState.busy); + // ✅ Inject ViewModels into ChatVM (only needs to be done once, but safe to repeat) chatVM.setViewModels(requestsVM: requestsVM, adVM: adVM); - - await requestsVM.populateDataForRequestsFilter(); - await requestsVM.populateDeliveryOptionEnums(); - await appointmentVM.populateAppointmentsFilterList(); - await adVM.populateAdsFilterList(); - await serviceVM.populateBranchServiceFilters(); - await serviceVM.getBranchAndServices(); - await requestsVM.getRequests(); - await subscriptionsVM.getSubscriptionBySP(AppState().getUser.data?.userInfo?.providerId.toString() ?? "", true); - - if (dashboardRouteEnum != DashboardRouteEnum.fromAdsPayment && dashboardRouteEnum != DashboardRouteEnum.fromAdsSubmit) { - await adVM.getMyAds(); - await adVM.getExploreAds(); + + // Call independent filter population APIs in parallel + await Future.wait([ + requestsVM.populateDataForRequestsFilter(), + requestsVM.populateDeliveryOptionEnums(), + appointmentVM.populateAppointmentsFilterList(), + adVM.populateAdsFilterList(), + serviceVM.populateBranchServiceFilters(), + shippingManagementVM.populateShippingRequestFilterList(), + shippingManagementVM.populateSelfPickupRequestFilterList(), + adVM.getVehicleTypes(), + adVM.getVehicleAdsDuration(), + ]); + + // Call data APIs in parallel + await Future.wait([ + serviceVM.getBranchAndServices(), + requestsVM.getRequests(), + subscriptionsVM.getSubscriptionBySP(AppState().getUser.data?.userInfo?.providerId.toString() ?? "", true), + appointmentVM.applyFilterOnAppointmentsVMForProviders( + appointmentStatusEnum: AppointmentStatusEnum.allAppointments, + isNeedCustomerFilter: true, + shouldPopulateUpcoming: true, + ), + ]); + + // Call ads APIs in parallel (conditionally) + if (dashboardRouteEnum != DashboardRouteEnum.fromAdsPayment && dashboardRouteEnum != DashboardRouteEnum.fromAdsSubmit && dashboardRouteEnum != DashboardRouteEnum.fromReserveAdsPayment) { + await Future.wait([ + adVM.getMyAds(), + adVM.getExploreAds(), + ]); } - await shippingManagementVM.populateShippingRequestFilterList(); - await shippingManagementVM.populateSelfPickupRequestFilterList(); - await appointmentVM.applyFilterOnAppointmentsVMForProviders( - appointmentStatusEnum: AppointmentStatusEnum.allAppointments, - isNeedCustomerFilter: true, - shouldPopulateUpcoming: true, - ); - - await adVM.getVehicleTypes(); - await adVM.getVehicleAdsDuration(); + + // Handle reserve ads payment completion + if (dashboardRouteEnum == DashboardRouteEnum.fromReserveAdsPayment) { + adVM.updateIsExploreAds(false); + onNavbarTapped(3); // Go to My Ads tab + await adVM.applyFilterOnMyAds(adPostStatusEnum: AdPostStatus.reserved); + log("✅ Successfully navigated to Reserved Ads after payment"); + } + + // Build chat hub connection last await chatVM.buildHubConnection(context: context); } diff --git a/lib/view_models/payment_view_model.dart b/lib/view_models/payment_view_model.dart index 44c8d54..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,119 +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"); - - // Always use navigatorKey for reliable navigation after payment - // Payment flow often unmounts context, so navigatorKey is more reliable - log("Navigating to dashboard with reserved ads filter"); - - // Navigate to dashboard first - navigatorKey.currentState?.pushNamedAndRemoveUntil( - AppRoutes.dashboard, - (route) => false, - arguments: DashboardRouteEnum.fromReserveAdsPayment, - ); + log("🔵 onAdReservePaymentSuccess called"); - // Use a post-frame callback to ensure navigation completes before updating state - WidgetsBinding.instance.addPostFrameCallback((_) { - final context = navigatorKey.currentContext; - if (context != null && context.mounted) { - try { - // Navigate to My Ads tab and apply Reserved filter - context.read().updateIsExploreAds(false); - if (AppState().currentAppType == AppType.provider) { - context.read().onNavbarTapped(3); // My Ads tab - context.read().applyFilterOnMyAds(adPostStatusEnum: AdPostStatus.reserved); - } else { - context.read().onNavbarTapped(3); // My Ads tab - context.read().applyFilterOnMyAds(adPostStatusEnum: AdPostStatus.reserved); - } - log("✅ Successfully navigated to Reserved Ads"); - } catch (e) { - log("⚠️ Error updating AdVM after navigation: $e"); - } - } else { - log("⚠️ Context not available for post-navigation updates"); + // 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 586b3fa..8446178 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/subscriptions_view_model.dart b/lib/view_models/subscriptions_view_model.dart index 21a87de..642492b 100644 --- a/lib/view_models/subscriptions_view_model.dart +++ b/lib/view_models/subscriptions_view_model.dart @@ -85,7 +85,8 @@ class SubscriptionsVM extends BaseVM { } } - Future payForSubscription(int subscriptionId, bool isStartNow, bool isReview, String amount, {bool isDegrade = false, List? listOfBranches, List? listOfUsers}) async { + Future payForSubscription( + {required int subscriptionId, required bool isStartNow, required bool isRenew, required String amount, bool isDegrade = false, List? listOfBranches, List? listOfUsers}) async { Map map; if (isDegrade) { map = { @@ -95,7 +96,7 @@ class SubscriptionsVM extends BaseVM { "subscriptionID": subscriptionId.toString(), "isStartNow": isStartNow.toString(), "subscriptionAmount": amount, - "isRenew": isReview.toString(), + "isRenew": isRenew.toString(), "listOfBranches": listOfBranches, "listOfUsers": listOfUsers }; @@ -107,7 +108,7 @@ class SubscriptionsVM extends BaseVM { "subscriptionID": subscriptionId.toString(), "isStartNow": isStartNow.toString(), "subscriptionAmount": amount, - "isRenew": isReview.toString() + "isRenew": isRenew.toString() // "listOfBranches": [], // "listOfUsers": [] }; @@ -116,8 +117,14 @@ class SubscriptionsVM extends BaseVM { return genericRespModel; } - Future createSubscriptionOrder(int subscriptionId, bool isStartNow, bool isRenew, {bool isDegrade = false, List? listOfBranches, List? listOfUsers}) async { - Map map = {"providerID": AppState().getUser.data?.userInfo?.providerId.toString() ?? "", "subscriptionID": subscriptionId.toString(), "isStartNow": isStartNow.toString(), "isRenew": isRenew.toString()}; + Future createSubscriptionOrder( + {required int subscriptionId, required bool isStartNow, required bool isRenew, bool isDegrade = false, List? listOfBranches, List? listOfUsers}) async { + Map map = { + "providerID": AppState().getUser.data?.userInfo?.providerId.toString() ?? "", + "subscriptionID": subscriptionId.toString(), + "isStartNow": isStartNow.toString(), + "isRenew": isRenew.toString() + }; GenericRespModel genericRespModel = await subscriptionRepo.payForProviderSubscription(map); return genericRespModel; } @@ -190,9 +197,7 @@ class SubscriptionsVM extends BaseVM { mySubscriptionsBySp.clear(); setState(ViewState.busy); // allSubscriptions.data - print("====================== SUB ============="); for (var element in allSubscriptions.data!) { - print("SuBBB "+ element.subscriptionType.toString()); if (element.subscriptionTypeEnum == SubscriptionTypeEnum.current) { mySubscriptionsBySp.add(element); } 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 9bcd44e..39d0be5 100644 --- a/lib/views/advertisement/ad_creation_steps/vehicle_details_container.dart +++ b/lib/views/advertisement/ad_creation_steps/vehicle_details_container.dart @@ -190,7 +190,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)), @@ -266,8 +272,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, @@ -286,13 +292,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 4f603e5..2b74524 100644 --- a/lib/views/advertisement/ads_detail_view/ads_detail_view.dart +++ b/lib/views/advertisement/ads_detail_view/ads_detail_view.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:developer'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:mc_common_app/classes/app_state.dart'; @@ -50,9 +51,10 @@ class _AdsDetailViewState extends State { } Future onAdDetailsLoaded() async { + log("${widget.adDetails.isMyAd}, ${widget.adDetails.isReservedByMe} ,${widget.adDetails.adPostStatus}"); context.read().updateCurrentAdId(id: widget.adDetails.id!); // Load banking info when viewing MY created ad that someone else reserved - if ((widget.adDetails.isMyAd ?? false) && !(widget.adDetails.isReservedByMe ?? false) && (widget.adDetails.adPostStatus == AdPostStatus.reserved)) { + if ((widget.adDetails.isMyAd ?? false) && (widget.adDetails.isReservedByMe ?? false) && (widget.adDetails.adPostStatus == AdPostStatus.reserved)) { await adVM.getAdBankingAccountInfo(adId: widget.adDetails.id!); } } @@ -146,7 +148,7 @@ class _AdsDetailViewState extends State { Row( children: [ ("${LocaleKeys.mileage.tr()}: ").toText(fontSize: 12, color: MyColors.lightTextColor), - "${widget.adDetails.vehicle!.mileage!.mileageEnd} ${LocaleKeys.km.tr()}".toText(fontSize: 12), + "${widget.adDetails.vehicle?.mileage?.mileageStart ?? ""} - ${widget.adDetails.vehicle?.mileage?.mileageEnd ?? ""} (${LocaleKeys.km.tr()})".toText(fontSize: 12), ], ), widget.adDetails.createdOn != null @@ -166,12 +168,14 @@ class _AdsDetailViewState extends State { "${widget.adDetails.vehicle!.odometer ?? 0} ${LocaleKeys.km.tr()}".toText(fontSize: 12), ], ), - Row( - children: [ - ("${LocaleKeys.vehicleCondition.tr()}: ").toText(fontSize: 12, color: MyColors.lightTextColor), - (widget.adDetails.vehicle!.isInUsed == 0 ? LocaleKeys.brandNew.tr() : LocaleKeys.used.tr()).toText(fontSize: 12), - ], - ), + if (widget.adDetails.vehicle!.condition != null && widget.adDetails.vehicle!.condition?.id != null) ...[ + Row( + children: [ + ("${LocaleKeys.vehicleCondition.tr()}: ").toText(fontSize: 12, color: MyColors.lightTextColor), + (widget.adDetails.vehicle!.condition?.id == 2 ? LocaleKeys.used.tr() : LocaleKeys.used.tr()).toText(fontSize: 12), + ], + ), + ], Row( children: [ ("${LocaleKeys.accidentFree.tr()}: ").toText(fontSize: 12, color: MyColors.lightTextColor), @@ -615,7 +619,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, ); @@ -691,7 +697,7 @@ class ShareAdButton extends StatelessWidget { // Vehicle details shareMessage += '${vehicle.vehicleTitle ?? ""}\n'; shareMessage += '${LocaleKeys.model.tr()}: ${vehicle.modelyear?.label ?? ""}\n'; - shareMessage += '${LocaleKeys.mileage.tr()}: ${vehicle.mileage?.mileageEnd ?? ""}\n'; + shareMessage += '${LocaleKeys.mileage.tr()}: ${vehicle.mileage?.mileageStart ?? ""} - ${vehicle.mileage?.mileageEnd ?? ""}\n'; shareMessage += '${LocaleKeys.price.tr()}: ${vehicle.demandAmount?.toInt()} ${vehicle.currency ?? ""}\n'; if (vehicle.cityName != null && vehicle.cityName!.isNotEmpty) { shareMessage += '${LocaleKeys.location.tr()}: ${vehicle.cityName}\n'; diff --git a/lib/views/advertisement/ads_detail_view/components.dart b/lib/views/advertisement/ads_detail_view/components.dart index 1d8cdf0..d609433 100644 --- a/lib/views/advertisement/ads_detail_view/components.dart +++ b/lib/views/advertisement/ads_detail_view/components.dart @@ -47,6 +47,25 @@ class BuildAdDetailsActionButtonForExploreAds extends StatelessWidget { description: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + // Show instructions if available + if (adDetailsModel.reserveCustomerMessage != null && adDetailsModel.reserveCustomerMessage!.isNotEmpty) ...[ + LocaleKeys.shippingManagementInstruction.tr().toText(fontSize: 16, isBold: true), + 8.height, + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: MyColors.lightGreyBgColor, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: MyColors.borderColor), + ), + child: adDetailsModel.reserveCustomerMessage!.toText( + fontSize: 14, + maxLines: 10, + height: 1.5, + ), + ), + 20.height, + ], Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -91,7 +110,7 @@ class BuildAdDetailsActionButtonForExploreAds extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - LocaleKeys.carInsuranceService.tr().toText(fontSize: 14, isBold: true, color: MyColors.lightTextColor, fontWeight: FontWeight.w500), + LocaleKeys.carInsuranceService.tr().toText(fontSize: 14, color: MyColors.lightTextColor, fontWeight: FontWeight.w500), LocaleKeys.toBeDecided.tr().toText(fontSize: 12, isBold: true), ], ), @@ -99,7 +118,7 @@ class BuildAdDetailsActionButtonForExploreAds extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - LocaleKeys.registrationCarPlates.tr().toText(fontSize: 14, isBold: true, color: MyColors.lightTextColor), + LocaleKeys.registrationCarPlates.tr().toText(fontSize: 14, color: MyColors.lightTextColor), LocaleKeys.toBeDecided.tr().toText(fontSize: 12, isBold: true), ], ), @@ -107,7 +126,7 @@ class BuildAdDetailsActionButtonForExploreAds extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - LocaleKeys.homeDeliveryService.tr().toText(fontSize: 14, isBold: true, color: MyColors.lightTextColor), + LocaleKeys.homeDeliveryService.tr().toText(fontSize: 14, color: MyColors.lightTextColor), LocaleKeys.toBeDecided.tr().toText(fontSize: 12, isBold: true), ], ), @@ -262,7 +281,13 @@ class BuildAdDetailsActionButtonForExploreAds extends StatelessWidget { case CreatedByRoleEnum.provider: return defaultActionForProviderAndCustomer(context, adDetailsModel); case CreatedByRoleEnum.admin: - return reserveAdAction(context, adDetailsModel); + // Check if admin ad is reservable + if (adDetailsModel.isReservable == true && adDetailsModel.adPostStatus != AdPostStatus.sold) { + return reserveAdAction(context, adDetailsModel); + } else { + // Show contact buttons just like customer/provider ads + return defaultActionForProviderAndCustomer(context, adDetailsModel); + } case CreatedByRoleEnum.allAds: return const SizedBox.shrink(); } @@ -760,7 +785,7 @@ class BuildAdDetailsActionButtonForMyAds extends StatelessWidget { Future buildCancelReservationBottomSheet(BuildContext context, {required AdDetailsModel adDetails}) { // Capture parent context that will remain valid final BuildContext parentContext = context; - + return showModalBottomSheet( context: context, isScrollControlled: true, @@ -799,18 +824,22 @@ class BuildAdDetailsActionButtonForMyAds extends StatelessWidget { // Save reason before showing confirmation dialog final String cancelReason = adVM.reservationCancelReason; return actionConfirmationBottomSheet( - context: parentContext, // Use parent context + context: parentContext, + // Use parent context title: LocaleKeys.doWantCancelReservation.tr().toText(fontSize: 28, isBold: true, letterSpacing: -1.44), - subtitle: "yourAdVisibletoBuy.".tr(), + subtitle: LocaleKeys.yourAdVisibletoBuy.tr(), actionButtonYes: Expanded( child: ShowFillButton( maxHeight: 55, title: LocaleKeys.yes.tr(), fontSize: 15, onPressed: () async { - Navigator.pop(parentContext); // Pop confirmation - // Call with parent context that's still valid - await adVM.cancelMyAdReservation(parentContext, adId: adDetails.id!, reason: cancelReason); + Navigator.pop(parentContext); // Pop confirmation + // Call with parent context that's still valid, pass reserve ID + await adVM.cancelMyAdReservation(parentContext, + adId: adDetails.id!, + adsReserveId: adDetails.adsReserveId ?? 0, // Pass reserve ID + reason: cancelReason); }, ), ), @@ -910,42 +939,93 @@ class BuildAdDetailsActionButtonForMyAds extends StatelessWidget { ), 15.height, LocaleKeys.attachFile.tr().toText(fontSize: 20, isBold: true, letterSpacing: -0.5), - if (adVM.pickedReceiptPdfFiles.isNotEmpty) ...[ - 16.height, - PickedFilesContainer( - pickedFiles: adVM.pickedReceiptPdfFiles, - onCrossPressedPrimary: adVM.removePdfFileFromList, - onAddFilePressed: () { - context.read().pickPdfReceiptFile(context); - }, - isPdf: true, - ), + 16.height, + if (adVM.pickedReceiptFile != null) ...[ + // Show selected file + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: MyColors.lightGreyBgColor, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: MyColors.borderColor), + ), + child: Row( + children: [ + Icon( + adVM.pickedReceiptFile!.filePath!.endsWith('.pdf') ? Icons.picture_as_pdf : Icons.image, + size: 40, + color: MyColors.primaryColor, + ), + 12.width, + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + adVM.pickedReceiptFile!.filePath!.split('/').last.toText(fontSize: 14, isBold: true, maxLines: 1), + 4.height, + LocaleKeys.change.tr().toText( + fontSize: 12, + color: MyColors.lightTextColor, + ), + ], + ), + ), + IconButton( + icon: const Icon(Icons.close, color: MyColors.redColor), + onPressed: () { + context.read().removeReceiptFile(); + }, + ), + ], + ), + ).onPress(() { + context.read().pickReceiptFile(context); + }), ] else ...[ + // Show add file button (original design) Row( children: [ Container( height: 90, width: 90, - decoration: BoxDecoration(color: MyColors.greyButtonColor, border: Border.all(width: 2, color: MyColors.greyAddBorderColor)), + decoration: BoxDecoration( + color: MyColors.greyButtonColor, + border: Border.all(width: 2, color: MyColors.greyAddBorderColor), + ), margin: const EdgeInsets.all(8), alignment: Alignment.center, child: Container( height: 24, width: 24, - decoration: const BoxDecoration(shape: BoxShape.circle, color: MyColors.darkTextColor), + decoration: const BoxDecoration( + shape: BoxShape.circle, + color: MyColors.darkTextColor, + ), child: const Icon(Icons.add, color: MyColors.white), ), ).onPress(() { - context.read().pickPdfReceiptFile(context); + context.read().pickReceiptFile(context); }), ], ), ], + if (adVM.receiptFileError.isNotEmpty) ...[ + 8.height, + adVM.receiptFileError.toText( + fontSize: 12, + color: MyColors.redColor, + ), + ], 15.height, ShowFillButton( title: LocaleKeys.submit.tr(), onPressed: () { - //Upload Attachment + if (adVM.pickedReceiptFile == null) { + adVM.receiptFileError = LocaleKeys.selectAttachment.tr(); + adVM.notifyListeners(); + return; + } + adVM.uploadBankReceipt(context, adId: adDetails.id!); }, maxWidth: double.infinity, ), @@ -1035,6 +1115,9 @@ class BuildAdDetailsActionButtonForMyAds extends StatelessWidget { } return pendingForReviewAction(pendingText: LocaleKeys.waitingAdminsResponse.tr()); + case AdReserveStatus.paymentDoneByCustomer: + return pendingForReviewAction(pendingText: LocaleKeys.waitingAdminsResponse.tr()); + case AdReserveStatus.fullPaymentVerified: return pendingForReviewAction(pendingText: LocaleKeys.paymentVerified.tr()); @@ -1045,8 +1128,6 @@ class BuildAdDetailsActionButtonForMyAds extends StatelessWidget { @override Widget build(BuildContext context) { - log("adDetailsModel.adPostStatus: ${adDetailsModel.adPostStatus}"); - switch (adDetailsModel.adPostStatus!) { case AdPostStatus.pendingForPayment: return pendingForPaymentAction(context, ad: adDetailsModel); diff --git a/lib/views/advertisement/ads_filter_view.dart b/lib/views/advertisement/ads_filter_view.dart index dab1f1d..05ec994 100644 --- a/lib/views/advertisement/ads_filter_view.dart +++ b/lib/views/advertisement/ads_filter_view.dart @@ -315,6 +315,8 @@ class _AdsFilterViewState extends State { onChanged: (value) => adVM.onCurrentAdMobilePhoneFilterChanged(value), value: adVM.currentAdMobilePhoneFilter, hint: LocaleKeys.enterMobileNumber.tr(), + keyboardType: TextInputType.phone, + numbersOnly: true, postfixWidget: Consumer( builder: (BuildContext context, AdVM adVM, Widget? child) { if (adVM.currentAdMobilePhoneFilter.isEmpty) { diff --git a/lib/views/advertisement/components/ads_list_widget.dart b/lib/views/advertisement/components/ads_list_widget.dart index 6b4b6c0..7b3a391 100644 --- a/lib/views/advertisement/components/ads_list_widget.dart +++ b/lib/views/advertisement/components/ads_list_widget.dart @@ -194,11 +194,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 @@ -236,7 +243,6 @@ class _AdCardState extends State { if (widget.adDetails.adPostStatus == AdPostStatus.sold) ...[ // Blur overlay ClipRRect( - borderRadius: BorderRadius.circular(8), child: BackdropFilter( filter: ImageFilter.blur(sigmaX: 3, sigmaY: 3), child: Container( @@ -249,10 +255,7 @@ class _AdCardState extends State { // SOLD text Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - decoration: BoxDecoration( - color: Colors.red, - borderRadius: BorderRadius.circular(4), - ), + decoration: const BoxDecoration(color: Colors.red), child: LocaleKeys.sold.tr().toText( fontSize: 12, color: Colors.white, @@ -276,25 +279,38 @@ class _AdCardState extends State { crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start, children: [ + // Status tags row (for explore ads) + if (isAdsFragment && context.read().isExploreAdsTapped) ...[ + Wrap( + spacing: 4, + runSpacing: 4, + children: [ + // Show "Mowater Ad" tag for admin ads + if (widget.adDetails.createdByRoleEnum == CreatedByRoleEnum.admin) Utils.statusContainerChip(text: "Mowater Ad", chipColor: MyColors.primaryColor), + // Show Accident Free chip when isAccidentFree is true + if (widget.adDetails.vehicle?.isAccidentFree == true) Utils.statusContainerChip(text: LocaleKeys.accidentFree.tr(), chipColor: Colors.green), + ], + ), + ], // When viewing reserved ads filter, show ONLY reserve status badge - if (widget.adDetails.isReservedByMe == true && - widget.adDetails.adReserveStatus != null && + if (widget.adDetails.isReservedByMe == true && + widget.adDetails.adReserveStatus != null && widget.adDetails.adReserveStatus != AdReserveStatus.defaultStatus && context.read().isReservedFilterSelected) ...[ Utils.statusContainerChip( - text: Utils.getReserveStatusLabel(widget.adDetails.adReserveStatus!), - chipColor: Utils.getChipColorByReserveStatus(widget.adDetails.adReserveStatus!) - ), + text: Utils.getReserveStatusLabel(widget.adDetails.adReserveStatus!), chipColor: Utils.getChipColorByReserveStatus(widget.adDetails.adReserveStatus!)), ] // Otherwise, show regular ad status for MyAds else if (isAdsFragment && widget.adDetails.adPostStatus! == AdPostStatus.sold && widget.adDetails.statuslabel != null && widget.adDetails.statuslabel!.isNotEmpty) ...[ - Utils.statusContainerChip(text: widget.adDetails.statuslabel ?? "", chipColor: Utils.getChipColorByAdStatus(widget.adDetails.adPostStatus!)), + Padding( + padding: const EdgeInsets.only(top: 5), + child: Utils.statusContainerChip(text: widget.adDetails.statuslabel ?? "", chipColor: Utils.getChipColorByAdStatus(widget.adDetails.adPostStatus!)), + ), ] else if (isAdsFragment && !context.read().isExploreAdsTapped && widget.adDetails.statuslabel != null && widget.adDetails.statuslabel!.isNotEmpty) ...[ - Utils.statusContainerChip(text: widget.adDetails.statuslabel ?? "", chipColor: Utils.getChipColorByAdStatus(widget.adDetails.adPostStatus!)), - ], - // Show Accident Free chip for explore ads when isAccidentFree is true - if (isAdsFragment && context.read().isExploreAdsTapped && widget.adDetails.vehicle?.isAccidentFree == true) ...[ - Utils.statusContainerChip(text: LocaleKeys.accidentFree.tr(), chipColor: Colors.green), + Padding( + padding: const EdgeInsets.only(top: 5), + child: Utils.statusContainerChip(text: widget.adDetails.statuslabel ?? "", chipColor: Utils.getChipColorByAdStatus(widget.adDetails.adPostStatus!)), + ), ], (widget.adDetails.vehicle!.vehicleTitle ?? "").toText( fontSize: 16, @@ -326,11 +342,14 @@ class _AdCardState extends State { height: 18 / 12, ), 2.width, - (widget.adDetails.vehicle!.mileage!.mileageEnd ?? "").toText( - fontSize: 12, - letterSpacing: -0.48, - height: 18 / 12, - ), + Expanded( + child: "${widget.adDetails.vehicle?.mileage?.mileageStart ?? ""} - ${widget.adDetails.vehicle?.mileage?.mileageEnd ?? ""}km".toText( + overflow: TextOverflow.ellipsis, + fontSize: 12, + letterSpacing: -0.48, + height: 18 / 12, + ), + ) ], ), ], @@ -340,16 +359,8 @@ class _AdCardState extends State { crossAxisAlignment: CrossAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.start, children: [ - (isArabic? widget.adDetails.vehicle!.cityNameN ?? "" : widget.adDetails.vehicle!.cityName ?? "").toText(color: MyColors.lightTextColor), - // widget.adDetails.createdOn != null ? DateTime.parse(widget.adDetails.createdOn!).getTimeAgo().toText(color: MyColors.lightTextColor) : const SizedBox(), - widget.adDetails.createdOn != null - ? DateFormat( - 'd MMM, y', - isArabic ? 'ar' : 'en', - ) - .format(DateTime.parse(widget.adDetails.createdOn!)) - .toText(color: MyColors.lightTextColor) - : const SizedBox(), + (widget.adDetails.vehicle!.cityName ?? "").toText(color: MyColors.lightTextColor, fontSize: 4), + widget.adDetails.createdOn != null ? DateTime.parse(widget.adDetails.createdOn!).getTimeAgo().toText(color: MyColors.lightTextColor, fontSize: 4) : const SizedBox(), ], ), ], diff --git a/lib/views/advertisement/select_ad_type_view.dart b/lib/views/advertisement/select_ad_type_view.dart index 82da2fa..65a7694 100644 --- a/lib/views/advertisement/select_ad_type_view.dart +++ b/lib/views/advertisement/select_ad_type_view.dart @@ -125,7 +125,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/book_appointment_services_view.dart b/lib/views/appointments/book_appointment_services_view.dart index a8cf8e3..cd1940e 100644 --- a/lib/views/appointments/book_appointment_services_view.dart +++ b/lib/views/appointments/book_appointment_services_view.dart @@ -120,10 +120,15 @@ class BookAppointmentServicesView extends StatelessWidget { LocaleKeys.sar.tr().toText(color: MyColors.lightTextColor, fontSize: 16, isBold: true).paddingOnly(bottom: 5), const Icon(Icons.arrow_drop_down, size: 30) ], - ).onPress(() => appointmentsVM.priceBreakDownClicked(context, serviceData)), + ), ], ], - ).toWhiteContainer(width: double.infinity, allPading: 12, margin: const EdgeInsets.symmetric(horizontal: 21, vertical: 10)); + ).toWhiteContainer( + width: double.infinity, + allPading: 12, + margin: const EdgeInsets.symmetric(horizontal: 21, vertical: 10), + onTap: () => appointmentsVM.priceBreakDownClicked(context, serviceData), + ); }, ).expand(), Row( diff --git a/lib/views/appointments/widgets/appointment_service_pick_bottom_sheet.dart b/lib/views/appointments/widgets/appointment_service_pick_bottom_sheet.dart index 61257df..9fd67d6 100644 --- a/lib/views/appointments/widgets/appointment_service_pick_bottom_sheet.dart +++ b/lib/views/appointments/widgets/appointment_service_pick_bottom_sheet.dart @@ -78,9 +78,7 @@ class AppointmentServicePickBottomSheet extends StatelessWidget { if (element.categoryId == appointmentsVM.branchSelectedCategoryId.selectedId) { serviceCategories.add(DropValue( element.serviceProviderServiceId ?? 0, - isArabic - ? element.serviceDescriptionN! - : element.serviceDescription!, + isArabic ? element.serviceDescriptionN! : element.serviceDescription!, // element.serviceDescription!, "", )); @@ -218,7 +216,9 @@ class AppointmentServicePickBottomSheet extends StatelessWidget { appointmentsVM.currentServiceSelection!.rangePricePerKm.toString().toText(fontSize: 30, isBold: true), "${LocaleKeys.sar.tr()}/km".toText(fontSize: 15, isBold: true, color: MyColors.lightTextColor).paddingOnly(bottom: 5), ] else ...[ - (appointmentsVM.currentServiceSelection!.servicelocationInfo.homeChargesInCurrentService * appointmentsVM.currentServiceSelection!.servicelocationInfo.distanceToBranch) + // Calculate using rounded values so displayed math matches the result + (double.parse(appointmentsVM.currentServiceSelection!.servicelocationInfo.homeChargesInCurrentService.toStringAsFixed(2)) * + double.parse(appointmentsVM.currentServiceSelection!.servicelocationInfo.distanceToBranch.toStringAsFixed(2))) .toStringAsFixed(2) .toText( fontSize: 30, @@ -226,7 +226,7 @@ class AppointmentServicePickBottomSheet extends StatelessWidget { ), LocaleKeys.sar.tr().toText(fontSize: 15, isBold: true, color: MyColors.lightTextColor).paddingOnly(bottom: 5), 10.width, - ("(${(appointmentsVM.currentServiceSelection!.servicelocationInfo.homeChargesInCurrentService).toStringAsFixed(2)} ${LocaleKeys.sar.tr()} x ${appointmentsVM.currentServiceSelection!.servicelocationInfo.distanceToBranch.toStringAsFixed(2)} KMs )") + ("(${appointmentsVM.currentServiceSelection!.servicelocationInfo.homeChargesInCurrentService.toStringAsFixed(2)} ${LocaleKeys.sar.tr()} x ${appointmentsVM.currentServiceSelection!.servicelocationInfo.distanceToBranch.toStringAsFixed(2)} KMs )") .toText(fontSize: 13, color: MyColors.lightTextColor) .paddingOnly(bottom: 5), ], 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..31a1491 100644 --- a/lib/views/profile/profile_view.dart +++ b/lib/views/profile/profile_view.dart @@ -127,6 +127,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/request_detail_page.dart b/lib/views/requests/request_detail_page.dart index 3e596d4..1d92815 100644 --- a/lib/views/requests/request_detail_page.dart +++ b/lib/views/requests/request_detail_page.dart @@ -150,9 +150,7 @@ class RequestDetailPage extends StatelessWidget { }) { switch (requestStatus) { case RequestStatusEnum.submitted: - String chatButtonText = requestTypeEnum == RequestsTypeEnum.specialCarRequest - ? LocaleKeys.specialCarRequestChat.tr() - : LocaleKeys.sparePartRequestChat.tr(); + String chatButtonText = requestTypeEnum == RequestsTypeEnum.specialCarRequest ? LocaleKeys.specialCarRequestChat.tr() : LocaleKeys.sparePartRequestChat.tr(); return ShowFillButton( maxWidth: double.infinity, @@ -185,7 +183,7 @@ class RequestDetailPage extends StatelessWidget { if (status) { await requestVM.getRequests(isNeedToRebuild: true); pop(context); - } + } }, ); } @@ -312,16 +310,17 @@ class RequestDetailPage extends StatelessWidget { appBar: CustomAppBar( title: LocaleKeys.requestDetail.tr(), actions: [ - requestDetailPageArguments.requestModel.isChatted - ? Padding( - padding: const EdgeInsets.only(top: 8, bottom: 8, right: 21), - child: const Icon(Icons.messenger_outline_rounded, color: Colors.black, size: 18).toContainer( - borderRadius: 100, - borderColor: MyColors.lightGreyEFColor, - isEnabledBorder: true, - ), - ).onPress(() => onViewChatTapped(context)) - : const SizedBox(), + // requestDetailPageArguments.requestModel.isChatted + // ? + Padding( + padding: const EdgeInsets.only(top: 8, bottom: 8, right: 21), + child: const Icon(Icons.messenger_outline_rounded, color: Colors.black, size: 18).toContainer( + borderRadius: 100, + borderColor: MyColors.lightGreyEFColor, + isEnabledBorder: true, + ), + ).onPress(() => onViewChatTapped(context)) + // : const SizedBox(), ], ), body: SizedBox( diff --git a/lib/views/requests/widget/request_item.dart b/lib/views/requests/widget/request_item.dart index 138b246..f11f026 100644 --- a/lib/views/requests/widget/request_item.dart +++ b/lib/views/requests/widget/request_item.dart @@ -33,8 +33,8 @@ class RequestItem extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.start, children: [ imageUrl.buildNetworkImage( - width: 80, - height: 80, + width: 60, + height: 60, fit: BoxFit.cover, ), 12.width, @@ -75,6 +75,8 @@ class RequestItem extends StatelessWidget { if (request.vehicleTypeName.isNotEmpty) ...[ showItem("${LocaleKeys.vehicleType.tr()}:", isArabic ? request.vehicleTypeNameN :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 d7e4b64..1ecc653 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, diff --git a/lib/widgets/extensions/extensions_widget.dart b/lib/widgets/extensions/extensions_widget.dart index e869e20..0d49049 100644 --- a/lib/widgets/extensions/extensions_widget.dart +++ b/lib/widgets/extensions/extensions_widget.dart @@ -1,3 +1,5 @@ +import 'dart:developer'; + import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; @@ -382,15 +384,14 @@ extension BuildSVG on String? { this!, errorBuilder: (BuildContext context, Object obj, StackTrace? s) { return Container( - height: height, - width: width, - decoration: BoxDecoration(border: Border.all(color: MyColors.roundedCrossBgColor)), - child: Icon( - Icons.image_not_supported_outlined, - color: MyColors.roundedCrossBgColor, - size: width, - ), - ); + height: height, + width: width, + decoration: BoxDecoration(border: Border.all(color: MyColors.roundedCrossBgColor)), + child: const Icon( + Icons.image_not_supported_outlined, + color: MyColors.roundedCrossBgColor, + // size: width, + )); }, loadingBuilder: (BuildContext context, Widget? child, ImageChunkEvent? imageChunk) { if (imageChunk == null) {