Merge remote-tracking branch 'origin/master' into fatima_dev

# Conflicts:
#	assets/langs/ar-SA.json
#	lib/generated/locale_keys.g.dart
pull/7/head
Fatimah.Alshammari 1 month ago
commit fdfbd244ab

@ -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" :"لا توجد اعلانات تطابق بحثك"
}

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

@ -124,17 +124,16 @@ class ApiClientImp implements ApiClient {
return await _postForResponse(url, requestBody, token: token, queryParameters: queryParameters, headers: headers, retryTimes: retryTimes);
}
Future<Response> _postForResponse(
String url,
requestBody, {
String? token,
Map<String, dynamic>? queryParameters,
Map<String, String>? headers,
int retryTimes = 0,
}) async {
Future<Response> _postForResponse(String url,
requestBody, {
String? token,
Map<String, dynamic>? queryParameters,
Map<String, String>? headers,
int retryTimes = 0,
}) async {
try {
var headers0 = <String, String>{};
// 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<T> _withClient<T>(Future<T> 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<Response> _getForResponse(String url, {String? token, Map<String, dynamic>? queryParameters, Map<String, String>? headers, int retryTimes = 0}) async {
try {
var headers0 = <String, String>{};
// Add Accept-Language header based on current locale
headers0['Accept-Language'] = _getCurrentLanguage();

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

@ -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<String,dynamic> _en_US = {
"firstTimeLogIn": "First Time Log In",
@ -937,6 +945,7 @@ static const Map<String,dynamic> _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<String,dynamic> _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<String,dynamic> _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<String,dynamic> _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<String,dynamic> _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<String,dynamic> _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",

@ -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';
}

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

@ -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<dynamic>? 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<dynamic>.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<dynamic>.from(serviceProviderBranch!.map((x) => x)),
"isVerified": isVerified,

@ -74,7 +74,7 @@ abstract class AdsRepo {
Future<GenericRespModel> deleteAd({required int adId});
Future<GenericRespModel> cancelMyAdReservation({required int adId, required int adsReserveStatus, required String reason});
Future<GenericRespModel> cancelMyAdReservation({required int adId, required int adsReserveId, required int adsReserveStatus, required String reason});
Future<GenericRespModel> createReserveAd({required int adId});
@ -451,6 +451,7 @@ class AdsRepoImp implements AdsRepo {
Future<List<AdDetailsModel>> 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<AdDetailsModel> 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<int, AdReserveStatus> statusMap = {};
Map<int, int> 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<GenericRespModel> cancelMyAdReservation({required int adId, required int adsReserveStatus, required String reason}) async {
Future<GenericRespModel> 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,
);

@ -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<String> 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<AndroidFlutterLocalNotificationsPlugin>()?.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<ChatVM>(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<ChatNavigationService>();
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',

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

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

@ -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<void> 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<void> cancelMyAdReservation(BuildContext context, {required int adId, required String reason}) async {
Future<void> 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<ImageModel> 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<ImageModel> imageModels = [];
List<File>? 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<File>? 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<void> 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<int> 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<String> 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<String> 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();

File diff suppressed because it is too large Load Diff

@ -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<void> onNewMessageReceivedForAds({required List<ChatMessageModel> 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 = <String, dynamic>{
"ReceiverUserID": receiverId,
// "SenderUserID": userId,
"SenderUserID": userId, // ✅ Send SenderUserID so backend knows who sent it
"MessageType": chatMessageType.getIdFromChatMessageTypeEnum(),
"ChatText": message,
"AdsID": adId,

@ -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<RequestsVM>(context, listen: false);
ChatVM chatVM = Provider.of<ChatVM>(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<dynamic>([
appointmentsVM.populateBranchesFilterList(),
appointmentsVM.populateAppointmentsFilterList(),
adVM.populateAdsFilterList(),
requestsVM.populateDataForRequestsFilter(),
requestsVM.populateDeliveryOptionEnums(),
adVM.getVehicleTypes(),
adVM.getVehicleAdsDuration(),
]);
// Call data APIs in parallel
await Future.wait<dynamic>([
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<dynamic>([
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) {

@ -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<ChatVM>(_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<ServiceVM>();
final adVM = context.read<AdVM>();
final subscriptionsVM = context.read<SubscriptionsVM>();
// 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<dynamic>([
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<dynamic>([
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<dynamic>([
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);
}

@ -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<void> onContinuePressed(BuildContext context, {required PaymentTypes paymentType}) async {
// Cache AdVM reference for updating local state after payment
try {
_cachedAdVM = context.read<AdVM>();
} 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<DashboardVMProvider>().onNavbarTapped(4);
} else {
context.read<DashboardVmCustomer>().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<AdVM>().updateIsExploreAds(false);
if (AppState().currentAppType == AppType.provider) {
context.read<DashboardVMProvider>().onNavbarTapped(3);
context.read<AdVM>().applyFilterOnMyAds(adPostStatusEnum: AdPostStatus.active);
} else {
context.read<DashboardVmCustomer>().onNavbarTapped(3);
context.read<AdVM>().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<AdVM>().updateIsExploreAds(false);
if (AppState().currentAppType == AppType.provider) {
context.read<DashboardVMProvider>().onNavbarTapped(3); // My Ads tab
context.read<AdVM>().applyFilterOnMyAds(adPostStatusEnum: AdPostStatus.reserved);
} else {
context.read<DashboardVmCustomer>().onNavbarTapped(3); // My Ads tab
context.read<AdVM>().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<SubscriptionsVM>().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<DashboardVMProvider>().onNavbarTapped(1);
} else {
context.read<DashboardVmCustomer>().onNavbarTapped(1);
}
navigateReplaceWithNameUntilRoute(context, AppRoutes.dashboard);
log("✅ Navigation executed successfully");
});
}
Future<void> onVisaCardSelected(BuildContext context, PaymentTypes paymentType, {Function()? onExit}) async {

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

@ -85,7 +85,8 @@ class SubscriptionsVM extends BaseVM {
}
}
Future<GenericRespModel> payForSubscription(int subscriptionId, bool isStartNow, bool isReview, String amount, {bool isDegrade = false, List<int>? listOfBranches, List<int>? listOfUsers}) async {
Future<GenericRespModel> payForSubscription(
{required int subscriptionId, required bool isStartNow, required bool isRenew, required String amount, bool isDegrade = false, List<int>? listOfBranches, List<int>? listOfUsers}) async {
Map<String, dynamic> 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<GenericRespModel> createSubscriptionOrder(int subscriptionId, bool isStartNow, bool isRenew, {bool isDegrade = false, List<int>? listOfBranches, List<int>? listOfUsers}) async {
Map<String, dynamic> map = {"providerID": AppState().getUser.data?.userInfo?.providerId.toString() ?? "", "subscriptionID": subscriptionId.toString(), "isStartNow": isStartNow.toString(), "isRenew": isRenew.toString()};
Future<GenericRespModel> createSubscriptionOrder(
{required int subscriptionId, required bool isStartNow, required bool isRenew, bool isDegrade = false, List<int>? listOfBranches, List<int>? listOfUsers}) async {
Map<String, dynamic> 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);
}

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

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

@ -190,7 +190,13 @@ class VehicleDetails extends StatelessWidget {
Builder(builder: (context) {
List<DropValue> 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,
),
],
],

@ -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<AdsDetailView> {
}
Future<void> onAdDetailsLoaded() async {
log("${widget.adDetails.isMyAd}, ${widget.adDetails.isReservedByMe} ,${widget.adDetails.adPostStatus}");
context.read<PaymentVM>().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<AdsDetailView> {
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<AdsDetailView> {
"${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<FavoriteAdButton> with SingleTickerPr
_animationController.forward().then((_) => _animationController.reverse());
bool success = await context.read<AdVM>().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';

@ -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<AdVM>().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<AdVM>().removeReceiptFile();
},
),
],
),
).onPress(() {
context.read<AdVM>().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<AdVM>().pickPdfReceiptFile(context);
context.read<AdVM>().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);

@ -315,6 +315,8 @@ class _AdsFilterViewState extends State<AdsFilterView> {
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) {

@ -194,11 +194,18 @@ class _AdCardState extends State<AdCard> {
);
},
)
: 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<AdCard> {
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<AdCard> {
// 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<AdCard> {
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
// Status tags row (for explore ads)
if (isAdsFragment && context.read<AdVM>().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<AdVM>().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<AdVM>().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<AdVM>().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<AdCard> {
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<AdCard> {
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(),
],
),
],

@ -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 ...[

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

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

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

@ -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<ChatView> {
}
chatVM = context.read<ChatVM>();
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<ChatView> {
@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<ChatView> {
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<PaymentVM>().updateRequestId(id: requestVM.currentSelectedRequest!.id);
requestVM.selectedDeliveryOptionEnum = null;
navigateWithName(context, AppRoutes.reviewRequestOffer, arguments: PaymentTypes.request);
},
isLoading: requestVM.isRefreshingAfterAccept,
onPressed: requestVM.isRefreshingAfterAccept
? null
: () {
context.read<PaymentVM>().updateRequestId(id: requestVM.currentSelectedRequest!.id);
requestVM.selectedDeliveryOptionEnum = null;
navigateWithName(context, AppRoutes.reviewRequestOffer, arguments: PaymentTypes.request);
},
),
),
]

@ -252,9 +252,18 @@ class _ChatMessageCustomWidgetState extends State<ChatMessageCustomWidget> {
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<ChatMessageCustomWidget> {
}
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);
}

@ -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<RequestsVM>();
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,
);

@ -127,6 +127,17 @@ class _ProfileScreenState extends State<ProfileScreen> {
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 ?? ""}",

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

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

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

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

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

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

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

Loading…
Cancel
Save