Fixed Bugs

master
FaizHashmi 1 month ago
parent f75c1a1130
commit 7669b6eca3

@ -12,6 +12,7 @@
"verify": "تحقق",
"verified": "تم التحقق",
"signOut": "تسجيل خروج",
"accessDenied": "الوصول مرفوض",
"enterEmail": "ادخل الايميل",
"enterNewEmail": "ادخل ايميل جديد",
"enterCurrentPassword": "كلمة المرور الحاليه",
@ -63,6 +64,8 @@
"imageUploaded": "تم تحميل الصوره",
"fileLarger": "1KB حجم الملف اكبر من",
"account": "الحساب",
"role": "الدور",
"userType": "نوع المستخدم",
"edit": "تعديل",
"imageDeleted": "تم حذف الصوره",
"dashboard": "الصفحه الرئيسيه",
@ -383,6 +386,7 @@
"appointmentBookedSuccessfully": "تم حجز موعدك بنجاح",
"appointmentStatusUpdated": "تم تحديث حالة الموعد",
"paymentStatusUpdated": "تم تحديث حالة الدفع",
"serviceStatusUpdated": "تم تحديث حالة الخدمة",
"selectOneSubService": "يرجى اختيار خدمة فرعية واحدة على الأقل",
"chargesBreakdown": "تفاصيل الرسوم",
"services": "الخدمات",
@ -662,6 +666,7 @@
"customerCarePrompt": "هل لديك أي استفسار؟ نحن هنا لخدمتك. انقر هنا للتحدث مع خدمة العملاء لدينا الآن.",
"callNow": "اتصل الآن",
"condition": "الحالة",
"newCondition": "جديد",
"servicesNotAvailableHomeLocation": "بعض الخدمات غير متوفرة في الموقع المنزلي.",
"additionalChargesNotice": "هذه الرسوم إضافية على رسوم الخدمة الفعلية. قد تختلف الرسوم للعناصر الثقيلة.",
"selectServiceLocation": "اختر موقع الخدمة",
@ -738,6 +743,7 @@
"markAdAsSoldDesc": "سيتم وضع علامة 'تم البيع' على هذا الإعلان، ولن يتمكن المستخدمون الآخرون من التواصل معك.",
"ownerInformation": "معلومات المالك",
"acceptedRequests": "الطلبات المقبولة",
"specialRequests": "الطلبات الخاصة",
"specialRequestChat": "دردشة الطلب الخاص",
"sparePartRequestChat": "دردشة طلب قطع الغيار",
"specialCarRequestChat": "دردشة طلب السيارة الخاصة",
@ -905,5 +911,8 @@
"androidApp": "أندرويد",
"iosApp": "آيفون",
"pleaseWait": "يرجى الانتظار...",
"numberOfShipping": "عدد الشحنات"
"numberOfShipping": "عدد الشحنات",
"loading": "جار التحميل...",
"pleaseSelectAccidentFree": "يرجى تحديد ما إذا كانت المركبة خالية من الحوادث",
"pleaseSelectMileageIncrease": "يرجى تحديد ما إذا كان يمكن زيادة المسافة المقطوعة"
}

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

@ -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,4 +893,8 @@ abstract class LocaleKeys {
static const iosApp = 'iosApp';
static const pleaseWait = 'pleaseWait';
static const numberOfShipping = 'numberOfShipping';
static const loading = 'loading';
static const pleaseSelectAccidentFree = 'pleaseSelectAccidentFree';
static const pleaseSelectMileageIncrease = 'pleaseSelectMileageIncrease';
}

@ -43,12 +43,13 @@ class AdDetailsModel {
String? modifiedOn;
AdPostStatus? adPostStatus;
AdReserveStatus? adReserveStatus;
int? adsReserveId; // Reserve ID from AdsReserve_Get API
int? adsReserveId; // Reserve ID from AdsReserve_Get API
bool? isReservable;
String? reserveCustomerMessage;
bool? isMyAd;
bool? isReservedByMe;
bool? isAdFavorite;
int? favouriteAdID; // Favorite record ID (used when unfavoriting)
String? phoneNo;
String? whatsAppNo;
String? adOwnerName;
@ -101,6 +102,7 @@ class AdDetailsModel {
this.isMyAd,
this.isReservedByMe,
this.isAdFavorite,
this.favouriteAdID,
this.phoneNo,
this.whatsAppNo,
this.adOwnerName,
@ -172,6 +174,7 @@ class AdDetailsModel {
isMyAd = isMyAds;
isReservedByMe = false;
isAdFavorite = json['isFavourite'] ?? false;
favouriteAdID = json['favouriteAdID'];
totalItemsCount = totalItems;
}
}

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

@ -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,25 @@ class FirebaseMessagingServiceImp implements FirebaseMessagingService {
}
});
// Create high-priority notification channel for Android (required for terminated state)
if (Platform.isAndroid) {
const AndroidNotificationChannel channel = AndroidNotificationChannel(
'mowater_chat_channel', // More descriptive channel ID
'Chat Notifications',
description: 'Notifications for chat messages',
importance: Importance.max, // Changed to max for Android 14
playSound: true,
enableVibration: true,
showBadge: true,
);
await flutterLocalNotifications
.resolvePlatformSpecificImplementation<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(
@ -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(
@ -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',

@ -27,6 +27,8 @@ enum DashboardRouteEnum {
fromAdsPayment,
fromAdsSubmit,
fromReserveAdsPayment,
fromAppointmentPayment,
fromRequestPayment,
none,
}

@ -82,6 +82,7 @@ class AdVM extends BaseVM {
String damagePartSearchValue = "";
String adReservePriceError = "";
String adPhoneNumberError = "";
String accidentFreeError = "";
// Edit Variables Amir
bool isExtendAdEditEnabled = false;
@ -820,10 +821,12 @@ class AdVM extends BaseVM {
notifyListeners();
}
int isInUsed = 0; // 0 = No, 1 = Yes
int? isInUsed; // null = not set, 0 = No, 1 = Yes
String isInUsedError = "";
void updateIsInUsed(int value) {
isInUsed = value;
isInUsedError = ""; // Clear error when user makes a selection
notifyListeners();
}
@ -833,6 +836,7 @@ class AdVM extends BaseVM {
void updateIsAccidentFree(bool value) {
isAccidentFree = value;
isAccidentFreeSet = true;
accidentFreeError = ""; // Clear error when user makes a selection
notifyListeners();
}
@ -1228,6 +1232,23 @@ class AdVM extends BaseVM {
} else {
vehicleDescError = "";
}
// Validate Accident Free field (mandatory)
if (!isAccidentFreeSet) {
accidentFreeError = LocaleKeys.pleaseSelectAccidentFree.tr();
isValidated = false;
} else {
accidentFreeError = "";
}
// Validate Is In Used field (mandatory)
if (isInUsed == null) {
isInUsedError = LocaleKeys.pleaseSelectMileageIncrease.tr();
isValidated = false;
} else {
isInUsedError = "";
}
if ((pickedPostingImages.isEmpty || pickedPostingImages.length < 3) && !isAdEditEnabled) {
vehicleImageError = GlobalConsts.attachImageError;
isValidated = false;
@ -1961,9 +1982,11 @@ class AdVM extends BaseVM {
adPhoneNumberDialCode = "";
adPhoneNumber = "";
odometer = "";
isInUsed = 0;
isInUsed = null;
isInUsedError = "";
isAccidentFree = false;
isAccidentFreeSet = false;
accidentFreeError = "";
clearSpecialServiceCard();
updateFinanceAvailableStatus(false);
notifyListeners();

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();
}
@ -119,19 +119,28 @@ class DashboardVmCustomer extends BaseVM with WidgetsBindingObserver {
await appointmentsVM.applyFilterOnBranches(index: 0); // to get all branches!
await appointmentsVM.getMyRecentBranches(); // to get my recent branches
if (dashboardRouteEnum != DashboardRouteEnum.fromAdsPayment &&
dashboardRouteEnum != DashboardRouteEnum.fromAdsSubmit &&
dashboardRouteEnum != DashboardRouteEnum.fromReserveAdsPayment) {
if (dashboardRouteEnum != DashboardRouteEnum.fromAdsPayment && dashboardRouteEnum != DashboardRouteEnum.fromAdsSubmit && dashboardRouteEnum != DashboardRouteEnum.fromReserveAdsPayment) {
await adVM.getMyAds();
await adVM.getExploreAds();
}
// Handle reserve ads payment completion
// Handle different payment completion scenarios
if (dashboardRouteEnum == DashboardRouteEnum.fromReserveAdsPayment) {
adVM.updateIsExploreAds(false);
onNavbarTapped(2); // Go to My Ads tab (customer has different tab index)
onNavbarTapped(3); // Go to My Ads tab
await adVM.applyFilterOnMyAds(adPostStatusEnum: AdPostStatus.reserved);
log("✅ Successfully navigated to Reserved Ads after payment");
} else if (dashboardRouteEnum == DashboardRouteEnum.fromAdsPayment) {
adVM.updateIsExploreAds(false);
onNavbarTapped(3); // Go to My Ads tab
await adVM.applyFilterOnMyAds(adPostStatusEnum: AdPostStatus.pendingForPost);
log("✅ Successfully navigated to My Ads after payment");
} else if (dashboardRouteEnum == DashboardRouteEnum.fromRequestPayment) {
onNavbarTapped(4); // Go to Requests tab
log("✅ Successfully navigated to Requests after payment");
} else if (dashboardRouteEnum == DashboardRouteEnum.fromAppointmentPayment) {
onNavbarTapped(1); // Go to Appointments tab
log("✅ Successfully navigated to Appointments after payment");
}
await requestsVM.getRequests();

@ -59,18 +59,18 @@ class DashboardVMProvider extends BaseVM with WidgetsBindingObserver {
switch (state) {
case AppLifecycleState.resumed:
// App came back to foreground - ensure SignalR is connected
// App came back to foreground - ensure SignalR is connected
logger.i("App resumed - checking SignalR connection");
chatVM.buildHubConnection(context: _context);
break;
case AppLifecycleState.paused:
// App went to background - keep connection alive (as per user requirement)
// App went to background - keep connection alive (as per user requirement)
logger.i("App paused - SignalR staying connected");
break;
case AppLifecycleState.inactive:
case AppLifecycleState.detached:
case AppLifecycleState.hidden:
// Other states - no action needed
// Other states - no action needed
break;
}
}
@ -80,7 +80,14 @@ class DashboardVMProvider extends BaseVM with WidgetsBindingObserver {
int selectedNavbarBarIndex = 2;
void onNavbarTapped(int index) async {
logger.i(AppState().getDeviceToken);
log("token: ${AppState().getDeviceToken.toString()}");
// Block access to Requests tab (index 4) for Individual Providers
// Only Dealerships can access Special Requests
if (index == 4 && AppState().userType != UserType.providerDealer) {
logger.w("Access denied: Individual providers cannot access Special Requests");
return;
}
selectedNavbarBarIndex = index;
notifyListeners();

@ -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,96 +208,129 @@ class PaymentVM extends ChangeNotifier {
}
void onRequestPaymentSuccess(BuildContext context) {
log("onRequestPaymentSuccess");
// Check if context is still mounted before using it
if (!context.mounted) {
log("Context not mounted, using navigatorKey instead");
// Use navigatorKey to navigate without context dependency
navigatorKey.currentState?.pushNamedAndRemoveUntil(
log("🔵 onRequestPaymentSuccess called");
// Wait a bit for browser to close and context to be ready
Future.delayed(const Duration(milliseconds: 500), () {
log("Attempting navigation after delay...");
final nav = navigatorKey.currentState;
if (nav == null) {
log("❌ navigatorKey.currentState is still null after delay");
return;
}
nav.pushNamedAndRemoveUntil(
AppRoutes.dashboard,
(route) => false,
arguments: DashboardRouteEnum.fromRequestPayment,
);
return;
}
if (AppState().currentAppType == AppType.provider) {
context.read<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");
// Navigate to dashboard with reserved ads route argument
// The dashboard's onRefresh method will handle setting up the reserved filter
log("Navigating to dashboard with reserved ads filter");
navigatorKey.currentState?.pushNamedAndRemoveUntil(
AppRoutes.dashboard,
(route) => false,
arguments: DashboardRouteEnum.fromReserveAdsPayment,
);
log("🔵 onAdReservePaymentSuccess called");
log("✅ Navigation initiated - Dashboard will handle filter setup");
// Wait a bit for browser to close and context to be ready
Future.delayed(const Duration(milliseconds: 500), () {
log("Attempting navigation after delay...");
final nav = navigatorKey.currentState;
if (nav == null) {
log("❌ navigatorKey.currentState is still null after delay");
return;
}
nav.pushNamedAndRemoveUntil(
AppRoutes.dashboard,
(route) => false,
arguments: DashboardRouteEnum.fromReserveAdsPayment,
);
log("✅ Navigation executed successfully");
});
}
void onSubscriptionPaymentSuccess() {
log("onSubscriptionPaymentSuccess");
// Use navigatorKey to navigate without context dependency
// This works even after payment browser is closed and context is unmounted
navigatorKey.currentState?.pushNamedAndRemoveUntil(
AppRoutes.dashboard,
(route) => false,
);
// context.read<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 = "";

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

@ -163,7 +163,10 @@ class VehicleDetails extends StatelessWidget {
Builder(builder: (context) {
List<DropValue> vehicleCategoriesDrop = [];
for (var element in adVM.vehicleCategories) {
vehicleCategoriesDrop.add(DropValue(element.id?.toInt() ?? 0, element.category ?? "", ""));
// Skip invalid items (id: 0 or null, empty category)
if (element.id != null && element.id! > 0 && (element.category?.isNotEmpty ?? false)) {
vehicleCategoriesDrop.add(DropValue(element.id!.toInt(), element.category!, ""));
}
}
return DropdownField(
(DropValue value) => adVM.updateSelectionVehicleCategoryId(SelectionModel(selectedId: value.id, selectedOption: value.value)),
@ -177,7 +180,13 @@ class VehicleDetails extends StatelessWidget {
Builder(builder: (context) {
List<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)),
@ -191,7 +200,10 @@ class VehicleDetails extends StatelessWidget {
Builder(builder: (context) {
List<DropValue> vehicleTransmissionsDrop = [];
for (var element in adVM.vehicleTransmissions) {
vehicleTransmissionsDrop.add(DropValue(element.id?.toInt() ?? 0, element.transmission ?? "", ""));
// Skip invalid items
if (element.id != null && element.id! > 0 && (element.transmission?.isNotEmpty ?? false)) {
vehicleTransmissionsDrop.add(DropValue(element.id!.toInt(), element.transmission!, ""));
}
}
return DropdownField(
(DropValue value) => adVM.updateSelectionVehicleTransmissionId(SelectionModel(selectedId: value.id, selectedOption: value.value)),
@ -206,7 +218,10 @@ class VehicleDetails extends StatelessWidget {
Builder(builder: (context) {
List<DropValue> vehicleCountriesDrop = [];
for (var element in adVM.vehicleCountries) {
vehicleCountriesDrop.add(DropValue(element.id?.toInt() ?? 0, element.countryName ?? "", ""));
// Skip invalid items
if (element.id != null && element.id! > 0 && (element.countryName?.isNotEmpty ?? false)) {
vehicleCountriesDrop.add(DropValue(element.id!.toInt(), element.countryName!, ""));
}
}
return DropdownField(
(DropValue value) => adVM.updateSelectionVehicleCountryId(SelectionModel(selectedOption: value.value, selectedId: value.id)),
@ -227,7 +242,10 @@ class VehicleDetails extends StatelessWidget {
Builder(builder: (context) {
List<DropValue> vehicleCitiesDrop = [];
for (var element in adVM.vehicleCities) {
vehicleCitiesDrop.add(DropValue(element.id?.toInt() ?? 0, element.cityName ?? "", ""));
// Skip invalid items
if (element.id != null && element.id! > 0 && (element.cityName?.isNotEmpty ?? false)) {
vehicleCitiesDrop.add(DropValue(element.id!.toInt(), element.cityName!, ""));
}
}
return DropdownField(
(DropValue value) => adVM.updateSelectionVehicleCityId(SelectionModel(selectedId: value.id, selectedOption: value.value)),
@ -247,8 +265,8 @@ class VehicleDetails extends StatelessWidget {
(DropValue value) => adVM.updateIsAccidentFree(value.id == 1),
list: accidentFreeDrop,
dropdownValue: adVM.isAccidentFreeSet ? DropValue(adVM.isAccidentFree ? 1 : 0, adVM.isAccidentFree ? LocaleKeys.yes.tr() : LocaleKeys.no.tr(), "") : null,
hint: LocaleKeys.hasYourCarHadAccident.tr(),
errorValue: "",
hint: LocaleKeys.accidentFree.tr(),
errorValue: adVM.accidentFreeError,
);
}),
8.height,
@ -267,13 +285,15 @@ class VehicleDetails extends StatelessWidget {
DropValue(1, LocaleKeys.yes.tr(), ""),
DropValue(0, LocaleKeys.no.tr(), ""),
],
dropdownValue: DropValue(
adVM.isInUsed,
adVM.isInUsed == 1 ? LocaleKeys.yes.tr() : LocaleKeys.no.tr(),
"",
),
dropdownValue: adVM.isInUsed != null
? DropValue(
adVM.isInUsed!,
adVM.isInUsed == 1 ? LocaleKeys.yes.tr() : LocaleKeys.no.tr(),
"",
)
: null,
hint: LocaleKeys.canMileageIncrease.tr(),
errorValue: "",
errorValue: adVM.isInUsedError,
),
],
],

@ -601,7 +601,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,
);

@ -193,11 +193,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

@ -122,7 +122,7 @@ class SelectAdTypeView extends StatelessWidget {
children: [
"${LocaleKeys.duration.tr()}: ".toText(fontSize: 12, color: MyColors.lightTextColor, isBold: true),
2.width,
LocaleKeys.validUntilSubscriptionExpiration.tr().toText(fontSize: 13),
LocaleKeys.validUntilSubscriptionExpiration.tr().toText(fontSize: 10),
],
).paddingOnly(top: 5, bottom: 5),
] else ...[

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

@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
import 'package:mc_common_app/classes/app_state.dart';
import 'package:mc_common_app/classes/consts.dart';
import 'package:mc_common_app/config/routes.dart';
import 'package:mc_common_app/extensions/enum_extensions.dart';
import 'package:mc_common_app/extensions/int_extensions.dart';
import 'package:mc_common_app/extensions/string_extensions.dart';
import 'package:mc_common_app/generated/locale_keys.g.dart';
@ -127,6 +128,17 @@ class _ProfileScreenState extends State<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 ?? ""}",

@ -32,8 +32,8 @@ class RequestItem extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.start,
children: [
imageUrl.buildNetworkImage(
width: 80,
height: 80,
width: 60,
height: 60,
fit: BoxFit.cover,
),
12.width,
@ -63,6 +63,8 @@ class RequestItem extends StatelessWidget {
if (request.vehicleTypeName.isNotEmpty) ...[
showItem("${LocaleKeys.vehicleType.tr()}:", request.vehicleTypeName),
],
// Show condition (New/Used)
showItem("${LocaleKeys.condition.tr()}:", request.isNew ? LocaleKeys.newCondition.tr() : LocaleKeys.used.tr()),
showItem("${LocaleKeys.description.tr()}:", request.description),
],
),

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

Loading…
Cancel
Save