Completed the bugs

faiz_development_common
FaizHashmi 1 week ago
parent 6f6533a1ca
commit c02319fefa

@ -722,6 +722,8 @@
"customerLocation": "موقع العميل",
"deliveryAvailable": "التوصيل متاح",
"viewed": "تم المشاهدة",
"read": "تمت القراءة",
"sent": "تم الإرسال",
"updateUserDetails": "تحديث تفاصيل المستخدم",
"enterNewFirstName": "أدخل الاسم الأول",
"enterNewLastName": "أدخل الاسم الأخير",
@ -834,6 +836,7 @@
"myDraftAds": "مسودتي للإعلانات",
"scheduleDeletedSuccessfully": "تم حذف الجدول بنجاح",
"addValidAddress": "رجى إضافة عنوان صالح",
"pleaseAddAtLeastOneImage": "يرجى إضافة صورة واحدة على الأقل لطلبك",
"termsPrivacyPolicy": "الشروط وسياسة الخصوصية",
"pendingApproval": "قيد الموافقة",
"inReview": "قيد المراجعة",

@ -474,7 +474,7 @@
"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",
@ -720,6 +720,8 @@
"customerLocation": "Customer Location",
"deliveryAvailable": "Delivery Available",
"viewed": "Viewed",
"read": "Read",
"sent": "Sent",
"itemNoLongerAvailable": "This item is no longer available.",
"reactivateAd": "Reactivate Ad",
"dealOutsideApp": "The deal was done outside the app with another customer.",
@ -832,6 +834,7 @@
"myDraftAds": "My Draft Ads",
"scheduleDeletedSuccessfully": "The schedule has been deleted successfully",
"addValidAddress": "Please add a valid address",
"pleaseAddAtLeastOneImage": "Please add at least one image for your request",
"termsPrivacyPolicy": "Terms & Privacy Policy",
"pendingApproval": "Pending Approval",
"inReview": "In Review",

@ -3,6 +3,7 @@ import 'dart:convert';
import 'dart:developer';
import 'dart:io';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart';
@ -70,6 +71,22 @@ abstract class ApiClient {
}
class ApiClientImp implements ApiClient {
/// Get current language code for Accept-Language header
/// Returns 'ar' for Arabic, 'en' for English (default)
String _getCurrentLanguage() {
try {
BuildContext? context = navigatorKey.currentContext;
if (context != null) {
String languageCode = context.locale.languageCode;
return languageCode == 'ar' ? 'ar' : 'en';
}
} catch (e) {
log("Error getting current language: $e");
}
// Default to English if context not available
return 'en';
}
@override
Future<U> postJsonForObject<T, U>(FactoryConstructor<U> factoryConstructor, String url, T jsonObject,
{String? token, Map<String, dynamic>? queryParameters, Map<String, String>? headers, int retryTimes = 0}) async {
@ -117,6 +134,10 @@ class ApiClientImp implements ApiClient {
}) async {
try {
var headers0 = <String, String>{};
// Add Accept-Language header based on current locale
headers0['Accept-Language'] = _getCurrentLanguage();
if (token != null) {
headers0['Authorization'] = 'Bearer $token';
}
@ -233,6 +254,10 @@ 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();
if (token != null) {
headers0['Authorization'] = 'Bearer $token';
}

@ -139,6 +139,7 @@ class ApiConsts {
static String adsPhotoOfficeAppointmentCreate = "${baseUrlServices}api/Advertisement/PhotoOfficeAppointment_Create";
static String adsMCBankAccountAdGet = "${baseUrlServices}api/Advertisement/MCBankAccountAd_Get";
static String adsReserveCreate = "${baseUrlServices}api/Advertisement/AdsReserve_Create";
static String adsReserveCancelByOwner = "${baseUrlServices}api/Advertisement/AdsReserve_CancelByOwner";
static String reserveAdPaymentOnDealDoneCreate = "${baseUrlServices}api/Advertisement/ReserveAdPaymentOnDealDone_Create";
static String adsExtendDurationCreate = "${baseUrlServices}api/Advertisement/Ads_ExtendDuration_Create";
static String adsSingleStepDraftCreate = "${baseUrlServices}api/Advertisement/AdsSingleStepDraft_Create";
@ -422,6 +423,7 @@ class SignalrConsts {
// Requests
static String receiveMessageRequestOffer = "ReceiveMessageRequestOffer";
static String sendMessageRequestOffer = "SendMessageRequestOffer";
static String requestOfferReadUpdate = "RequestOfferReadUpdate";
// Ads
static String sendMessageAds = "SendMessageAds";

@ -20,7 +20,8 @@ import 'package:mc_common_app/services/firebase_service.dart';
import 'package:mc_common_app/services/payments_service.dart';
import 'package:mc_common_app/utils/location/Location.dart';
import '../repositories/request_repo.dart';
import 'package:mc_common_app/repositories/request_repo.dart';
import 'package:mc_common_app/services/chat_navigation_service.dart';
Injector injector = Injector.appInstance;
@ -31,6 +32,7 @@ class AppDependencies {
injector.registerSingleton<ApiClient>(() => ApiClientImp());
injector.registerSingleton<CommonAppServices>(() => CommonServicesImp());
injector.registerSingleton<PaymentService>(() => PaymentServiceImp());
injector.registerSingleton<ChatNavigationService>(() => ChatNavigationServiceImp());
injector.registerSingleton<Location>(() => LocationService());
injector.registerSingleton<FirebaseMessagingService>(() => FirebaseMessagingServiceImp(
firebaseMessaging: FirebaseMessaging.instance,

@ -738,6 +738,8 @@ class CodegenLoader extends AssetLoader{
"customerLocation": "موقع العميل",
"deliveryAvailable": "التوصيل متاح",
"viewed": "تم المشاهدة",
"read": "تمت القراءة",
"sent": "تم الإرسال",
"updateUserDetails": "تحديث تفاصيل المستخدم",
"enterNewFirstName": "أدخل الاسم الأول",
"enterNewLastName": "أدخل الاسم الأخير",
@ -850,6 +852,7 @@ class CodegenLoader extends AssetLoader{
"myDraftAds": "مسودتي للإعلانات",
"scheduleDeletedSuccessfully": "تم حذف الجدول بنجاح",
"addValidAddress": "رجى إضافة عنوان صالح",
"pleaseAddAtLeastOneImage": "يرجى إضافة صورة واحدة على الأقل لطلبك",
"termsPrivacyPolicy": "الشروط وسياسة الخصوصية",
"pendingApproval": "قيد الموافقة",
"inReview": "قيد المراجعة",
@ -1642,6 +1645,8 @@ static const Map<String,dynamic> _en_US = {
"customerLocation": "Customer Location",
"deliveryAvailable": "Delivery Available",
"viewed": "Viewed",
"read": "Read",
"sent": "Sent",
"itemNoLongerAvailable": "This item is no longer available.",
"reactivateAd": "Reactivate Ad",
"dealOutsideApp": "The deal was done outside the app with another customer.",
@ -1754,6 +1759,7 @@ static const Map<String,dynamic> _en_US = {
"myDraftAds": "My Draft Ads",
"scheduleDeletedSuccessfully": "The schedule has been deleted successfully",
"addValidAddress": "Please add a valid address",
"pleaseAddAtLeastOneImage": "Please add at least one image for your request",
"termsPrivacyPolicy": "Terms & Privacy Policy",
"pendingApproval": "Pending Approval",
"inReview": "In Review",

@ -2,7 +2,7 @@
// ignore_for_file: constant_identifier_names
abstract class LocaleKeys {
abstract class LocaleKeys {
static const firstTimeLogIn = 'firstTimeLogIn';
static const signUp = 'signUp';
static const changeMobile = 'changeMobile';
@ -49,7 +49,7 @@ abstract class LocaleKeys {
static const emailChangedSuccessfully = 'emailChangedSuccessfully';
static const passwordIsUpdated = 'passwordIsUpdated';
static const passwordShouldContains = 'passwordShouldContains';
static const successfullyRegistered = ' successfullyRegistered';
static const successfullyRegistered = ' successfullyRegistered';
static const pleaseEnterSamePassword = 'pleaseEnterSamePassword';
static const firstNameMandatory = 'firstNameMandatory';
static const surnameNameMandatory = 'surnameNameMandatory';
@ -703,6 +703,8 @@ abstract class LocaleKeys {
static const customerLocation = 'customerLocation';
static const deliveryAvailable = 'deliveryAvailable';
static const viewed = 'viewed';
static const read = 'read';
static const sent = 'sent';
static const updateUserDetails = 'updateUserDetails';
static const enterNewFirstName = 'enterNewFirstName';
static const enterNewLastName = 'enterNewLastName';
@ -815,6 +817,7 @@ abstract class LocaleKeys {
static const myDraftAds = 'myDraftAds';
static const scheduleDeletedSuccessfully = 'scheduleDeletedSuccessfully';
static const addValidAddress = 'addValidAddress';
static const pleaseAddAtLeastOneImage = 'pleaseAddAtLeastOneImage';
static const termsPrivacyPolicy = 'termsPrivacyPolicy';
static const pendingApproval = 'pendingApproval';
static const inReview = 'inReview';
@ -884,5 +887,4 @@ abstract class LocaleKeys {
static const iosApp = 'iosApp';
static const pleaseWait = 'pleaseWait';
static const numberOfShipping = 'numberOfShipping';
}

@ -26,7 +26,7 @@ class MyReservedAdsRespModel {
adsID = json['adsID'];
customerID = json['customerID'];
paymentStatus = json['paymentStatus'];
paymentStatus = json['adsReserveStatus'];
adsReserveStatus = json['adsReserveStatus'];
reservationTimeID = json['reservationTimeID'];
reservationBasePrice = json['reservationBasePrice'];
refundAmount = json['refundAmount'];

@ -7,6 +7,7 @@ import 'package:mc_common_app/classes/consts.dart';
import 'package:mc_common_app/config/dependency_injection.dart';
import 'package:mc_common_app/extensions/string_extensions.dart';
import 'package:mc_common_app/generated/locale_keys.g.dart';
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';
@ -57,7 +58,11 @@ abstract class AdsRepo {
Future<List<AdDetailsModel>> getMyAds();
Future<List<AdDetailsModel>> getFavouriteAds();
Future<List<AdDetailsModel>> getFavouriteAds({
AdPostStatus? adPostStatus,
CreatedByRoleEnum? createdByRoleEnum,
int? vehicleBrandId,
});
Future<GenericRespModel> toggleAdFavorite({required int adsID, required bool isFavorite});
@ -455,16 +460,17 @@ class AdsRepoImp implements AdsRepo {
);
if (adsGenericModel.messageStatus != 1 || adsGenericModel.data == null) {
Utils.showToast(adsGenericModel.message ?? LocaleKeys.somethingWrong.tr());
return [];
throw Exception(adsGenericModel.message ?? LocaleKeys.somethingWrong.tr());
}
List<MyReservedAdsRespModel> reservedAds = List.generate(adsGenericModel.data.length, (index) => MyReservedAdsRespModel.fromJson(adsGenericModel.data[index]));
List<String> selectedIdsString = reservedAds.map((component) => component.adsID.toString()).toList();
if (selectedIdsString.isEmpty) {
return [];
if (reservedAds.isEmpty) {
return []; // No reserved ads - this is a valid state
}
List<String> selectedIdsString = reservedAds.map((component) => component.adsID.toString()).toList();
return await getAdsPerSpecificIds(ids: selectedIdsString, reservedAds: reservedAds);
}
@ -472,7 +478,7 @@ class AdsRepoImp implements AdsRepo {
var params = {
"AdsIDs": ids,
"isActive": "true",
"PageSize": "30",
"PageSize": ids.length.toString(), // Request all reserved ads in one call
};
GenericRespModel adsGenericModel = await apiClient.getJsonForObject(
token: appState.getUser.data!.accessToken,
@ -481,16 +487,49 @@ class AdsRepoImp implements AdsRepo {
ApiConsts.vehicleAdsGet,
);
if (adsGenericModel.messageStatus != 1 || adsGenericModel.data == null) {
Utils.showToast(adsGenericModel.message ?? LocaleKeys.somethingWrong.tr());
return [];
throw Exception(adsGenericModel.message ?? LocaleKeys.somethingWrong.tr());
}
List<AdDetailsModel> vehicleAdsDetails = List.generate(adsGenericModel.data.length, (index) => AdDetailsModel.fromJson(adsGenericModel.data[index], true, adsGenericModel.totalItemsCount ?? 1));
for (int i = 0; i < vehicleAdsDetails.length; i++) {
vehicleAdsDetails[i].adReserveStatus = (reservedAds[i].adsReserveStatus ?? 0).toAdRserveStatusEnum();
// Create a map for O(1) lookup: adsID -> reserve status
Map<int, AdReserveStatus> statusMap = {};
for (var reserved in reservedAds) {
log("adsReserveStatus: ${reserved.adsReserveStatus}");
statusMap[reserved.adsID ?? 0] = (reserved.adsReserveStatus ?? 0).toAdRserveStatusEnum();
}
log("📋 Status Map created: ${statusMap.length} entries");
statusMap.forEach((key, value) {
log(" Ad ID: $key -> Status: $value");
});
// Assign reserve status 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]!;
// Backend will set adPostStatus=reserved in the response, so no need to set it manually
log(" ✅ Ad ${ad.id}: Assigned adReserveStatus ${ad.adReserveStatus}");
} else {
// Ad exists but no reserve status found - shouldn't happen but handle it
ad.adReserveStatus = AdReserveStatus.defaultStatus;
log(" ⚠️ Ad ${ad.id}: No status found, set to defaultStatus");
}
}
// Show all ads with ANY reservation history (exclude only defaultStatus/none)
// This includes: reserved (1), cancelledByOwner (2), cancelledByAdmin (3), timeOver (4), dealDone (5), fullPaymentVerified (6)
int beforeFilter = vehicleAdsDetails.length;
vehicleAdsDetails.removeWhere((element) => element.adReserveStatus == AdReserveStatus.defaultStatus);
int afterFilter = vehicleAdsDetails.length;
log("🎯 Filter Result: ${afterFilter} ads remaining (removed ${beforeFilter - afterFilter})");
for (var ad in vehicleAdsDetails) {
log(" ✅ Keeping Ad ${ad.id} with status ${ad.adReserveStatus}");
}
vehicleAdsDetails.removeWhere((element) => element.adReserveStatus != AdReserveStatus.reserved);
return vehicleAdsDetails;
}
@ -515,16 +554,44 @@ class AdsRepoImp implements AdsRepo {
}
@override
Future<List<AdDetailsModel>> getFavouriteAds() async {
Future<List<AdDetailsModel>> getFavouriteAds({
AdPostStatus? adPostStatus,
CreatedByRoleEnum? createdByRoleEnum,
int? vehicleBrandId,
}) async {
// Build filter parameters similar to getAllAds
Map<String, dynamic> params = {
"PageSize": "30",
};
// Add status filter if provided
if (adPostStatus != null) {
params["AdsStatuses"] = ["${adPostStatus.getIdFromAdPostStatusEnum()}"];
}
// Add created by role filter if provided
if (createdByRoleEnum != null && createdByRoleEnum != CreatedByRoleEnum.allAds) {
params["CreatedByRoles"] = ["${createdByRoleEnum.getIdFromCreatedByRoleEnum()}"];
}
// Add vehicle brand filter if provided
if (vehicleBrandId != null && vehicleBrandId != 0) {
params["VehicleBrandIDs"] = ["$vehicleBrandId"];
}
GenericRespModel adsGenericModel = await apiClient.getJsonForObject(
token: appState.getUser.data!.accessToken,
(json) => GenericRespModel.fromJson(json),
ApiConsts.favouriteAdsGet,
queryParameters: params,
);
if (adsGenericModel.messageStatus != 1 || adsGenericModel.data == null) {
return [];
}
List<AdDetailsModel> favouriteAdsDetails = List.generate(adsGenericModel.data.length, (index) => AdDetailsModel.fromJson(adsGenericModel.data[index], false, adsGenericModel.totalItemsCount ?? 1));
return favouriteAdsDetails;
}
@ -638,7 +705,7 @@ class AdsRepoImp implements AdsRepo {
String token = appState.getUser.data!.accessToken ?? "";
GenericRespModel adsGenericModel = await apiClient.postJsonForObject(
(json) => GenericRespModel.fromJson(json),
ApiConsts.deleteAd,
ApiConsts.adsReserveCancelByOwner,
postParams,
token: token,
);

@ -0,0 +1,180 @@
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:mc_common_app/classes/app_state.dart';
import 'package:mc_common_app/config/routes.dart';
import 'package:mc_common_app/models/chat_models/buyers_chat_for_ads_model.dart';
import 'package:mc_common_app/models/requests_models/request_model.dart';
import 'package:mc_common_app/utils/enums.dart';
import 'package:mc_common_app/utils/navigator.dart';
/// Service for handling navigation to different chat types in the app
/// Provides centralized navigation logic for Request, Advertisement, and General chats
abstract class ChatNavigationService {
/// Navigate to Request Offer Chat screen
Future<void> navigateToRequestOfferChat({
required BuildContext context,
required int requestId,
required String receiverId,
required String senderId,
required int requestIndex,
required int providerIndex,
required RequestModel requestModel,
});
/// Navigate to Advertisement Chat screen
Future<void> navigateToAdChat({
required BuildContext context,
required int adsId,
required String receiverUserId,
});
/// Navigate to General Chat screen
Future<void> navigateToGeneralChat({
required BuildContext context,
required String receiverUserId,
Map<String, dynamic>? additionalData,
});
/// Navigate to Chat List screen based on chat type
Future<void> navigateToChatList({
required BuildContext context,
required ChatTypeEnum chatType,
Map<String, dynamic>? arguments,
});
}
class ChatNavigationServiceImp implements ChatNavigationService {
@override
Future<void> navigateToRequestOfferChat({
required BuildContext context,
required int requestId,
required String receiverId,
required String senderId,
required int requestIndex,
required int providerIndex,
required RequestModel requestModel,
}) async {
try {
log("Navigating to Request Offer Chat: requestId=$requestId");
ChatViewArgumentsForRequest args = ChatViewArgumentsForRequest(
chatTypeEnum: ChatTypeEnum.requestOffer,
requestId: requestId,
receiverId: receiverId,
senderId: senderId,
requestIndex: requestIndex,
providerIndex: providerIndex,
requestModel: requestModel,
);
ChatViewArguments chatViewArguments = ChatViewArguments(
chatTypeEnum: ChatTypeEnum.requestOffer,
chatViewArgumentsForRequest: args,
);
navigateWithName(context, AppRoutes.chatView, arguments: chatViewArguments);
} catch (e) {
log("Error navigating to request offer chat: $e");
}
}
@override
Future<void> navigateToAdChat({
required BuildContext context,
required int adsId,
required String receiverUserId,
}) async {
try {
log("Navigating to Ad Chat: adsId=$adsId, receiverId=$receiverUserId");
ChatViewArgumentsForAd args = ChatViewArgumentsForAd(
adsID: adsId,
receiverUserID: receiverUserId,
);
ChatViewArguments chatViewArguments = ChatViewArguments(
chatTypeEnum: ChatTypeEnum.ads,
chatViewArgumentsForAd: args,
);
navigateWithName(context, AppRoutes.chatView, arguments: chatViewArguments);
} catch (e) {
log("Error navigating to ad chat: $e");
}
}
@override
Future<void> navigateToGeneralChat({
required BuildContext context,
required String receiverUserId,
Map<String, dynamic>? additionalData,
}) async {
try {
log("Navigating to General Chat: receiverId=$receiverUserId");
// General chat implementation
// You can extend this based on your general chat requirements
ChatViewArguments chatViewArguments = ChatViewArguments(
chatTypeEnum: ChatTypeEnum.general,
);
navigateWithName(context, AppRoutes.chatView, arguments: chatViewArguments);
} catch (e) {
log("Error navigating to general chat: $e");
}
}
@override
Future<void> navigateToChatList({
required BuildContext context,
required ChatTypeEnum chatType,
Map<String, dynamic>? arguments,
}) async {
try {
log("Navigating to Chat List: type=$chatType");
switch (chatType) {
case ChatTypeEnum.requestOffer:
// Navigate to Providers Chat List (for customer side)
// or Requests list (for provider side)
if (AppState().currentAppType == AppType.customer) {
navigateWithName(context, AppRoutes.providersChatListPage);
} else {
// For provider, navigate to requests page
navigateWithName(context, AppRoutes.dashboard);
}
break;
case ChatTypeEnum.ads:
// Navigate to Ads Buyers Chat List
if (arguments != null && arguments['buyers'] != null) {
navigateWithName(
context,
AppRoutes.adsBuyerChatsListView,
arguments: arguments['buyers'] as List<BuyersChatForAdsModel>,
);
} else {
// Navigate to ads list if buyers not provided
navigateWithName(context, AppRoutes.dashboard);
}
break;
case ChatTypeEnum.general:
// Navigate to General Chats List
if (arguments != null && arguments['requestId'] != null) {
navigateWithName(
context,
AppRoutes.generalChatsListForProvider,
arguments: arguments['requestId'] as int,
);
} else {
navigateWithName(context, AppRoutes.dashboard);
}
break;
}
} catch (e) {
log("Error navigating to chat list: $e");
}
}
}

@ -1,13 +1,18 @@
import 'dart:convert';
import 'dart:developer';
import 'dart:io';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:mc_common_app/classes/app_state.dart';
import 'package:mc_common_app/config/dependency_injection.dart';
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:permission_handler/permission_handler.dart';
abstract class FirebaseMessagingService {
@ -63,17 +68,39 @@ class FirebaseMessagingServiceImp implements FirebaseMessagingService {
defaultPresentSound: true,
);
final InitializationSettings initializationSettings = InitializationSettings(android: initializationSettingsAndroid, iOS: initializationSettingsDarwin);
await flutterLocalNotifications.initialize(initializationSettings, onDidReceiveNotificationResponse: (notificationResponse) {}, );
await flutterLocalNotifications.initialize(
initializationSettings,
onDidReceiveNotificationResponse: (NotificationResponse response) async {
// Handle notification tap when app is in foreground/background
log("========== Notification Tapped ==========");
log("Notification ID: ${response.id}");
log("Action ID: ${response.actionId}");
log("Input: ${response.input}");
log("Payload: ${response.payload}");
log("=========================================");
if (response.payload != null && response.payload!.isNotEmpty) {
try {
Map<String, dynamic> data = jsonDecode(response.payload!);
log("Parsed payload data: $data");
_handleNotificationNavigation(data);
} catch (e) {
logger.e("Error parsing notification payload: $e");
}
}
},
);
RemoteMessage? initialMessage = await FirebaseMessaging.instance.getInitialMessage();
if (initialMessage != null) _handleNotification(initialMessage);
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
if (message.notification != null) _handleNotification(message);
// Foreground notification - just display, don't navigate automatically
if (message.notification != null) _handleForegroundNotification(message);
});
FirebaseMessaging.onMessageOpenedApp.listen(_handleNotification);
FirebaseMessaging.onMessageOpenedApp.listen(_handleNotificationOpenedApp);
FirebaseMessaging.onBackgroundMessage(backgroundMessageHandler);
@ -86,14 +113,208 @@ class FirebaseMessagingServiceImp implements FirebaseMessagingService {
logger.i(body);
}
void _handleForegroundNotification(RemoteMessage message) async {
log("========== Foreground Notification Received ==========");
log("Title: ${message.notification!.title}");
log("Body: ${message.notification!.body}");
log("Data payload: ${message.data}");
log("Message ID: ${message.messageId}");
log("Sent time: ${message.sentTime}");
log("======================================================");
// Just display notification - DON'T navigate automatically
// Navigation happens only when user taps the notification
displayNotification(
title: message.notification!.title ?? '',
body: message.notification!.body ?? '',
payload: jsonEncode(message.data),
);
}
void _handleNotificationOpenedApp(RemoteMessage message) async {
log("========== Notification Opened App ==========");
log("Title: ${message.notification!.title}");
log("Body: ${message.notification!.body}");
log("Data payload: ${message.data}");
log("Message ID: ${message.messageId}");
log("Sent time: ${message.sentTime}");
log("=============================================");
// User tapped notification - navigate to chat
_handleNotificationNavigation(message.data);
}
void _handleNotification(RemoteMessage message) async {
logger.i(message.notification!.title);
logger.i(message.notification!.body);
// This is called when app is terminated and opened by notification
log("========== App Terminated Notification ==========");
log("Title: ${message.notification!.title}");
log("Body: ${message.notification!.body}");
log("Data payload: ${message.data}");
log("Message ID: ${message.messageId}");
log("Sent time: ${message.sentTime}");
log("=================================================");
// User opened app from notification - navigate to chat
_handleNotificationNavigation(message.data);
}
void _handleNotificationNavigation(Map<String, dynamic> data) async {
log("========== Handling Notification Navigation ==========");
log("Full data received: $data");
if (data.isEmpty) {
log("Data is empty, skipping navigation");
log("======================================================");
return;
}
// 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) {
log("Invalid or missing NotiType, skipping navigation");
log("======================================================");
return;
}
// Map NotiType to chat type
String chatType;
switch (notiType) {
case 1:
chatType = 'request_offer';
break;
case 2:
chatType = 'ad_chat';
break;
case 3:
chatType = 'general_chat';
break;
default:
log("Unknown NotiType: $notiType");
log("======================================================");
return;
}
log("Mapped chat type: $chatType");
// Wait a bit for context to be available
await Future.delayed(Duration(milliseconds: 300));
BuildContext? context = navigatorKey.currentContext;
if (context == null) {
logger.e("Context not available for navigation");
log("======================================================");
return;
}
// Log all available data fields to understand backend format
log("Available data fields:");
data.forEach((key, value) {
log(" $key: $value");
});
switch (chatType) {
case 'request_offer':
log("Navigating to Request Offer Chat...");
await _navigateToRequestOfferChat(context, data);
break;
case 'ad_chat':
log("Navigating to Advertisement Chat...");
await _navigateToAdChat(context, data);
break;
case 'general':
log("Navigating to General Chat...");
await _navigateToGeneralChat(context, data);
break;
default:
logger.e("Unknown chat type: $chatType");
}
log("======================================================");
}
Future<void> _navigateToRequestOfferChat(
BuildContext context,
Map<String, dynamic> data,
) async {
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;
}
log("✅ Navigating to providers chat list for request: $requestId");
navigateWithName(context, AppRoutes.providersChatListPage);
// 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");
}
}
Future<void> _navigateToAdChat(
BuildContext context,
Map<String, dynamic> data,
) async {
try {
// Try both backend format and our format
int adId = int.tryParse(data['AdID']?.toString() ?? data['ad_id']?.toString() ?? '0') ?? 0;
String? receiverUserId = data['ReceiverUserID'] ?? data['receiver_id'];
displayNotification(title: message.notification!.title ?? '', body: message.notification!.body ?? '');
if (adId == 0 || receiverUserId == null) {
log("❌ Invalid AdID or ReceiverUserID in notification data");
log(" AdID: ${data['AdID']}, ReceiverUserID: ${data['ReceiverUserID']}");
return;
}
log("✅ Navigating to ad chat: adId=$adId");
final chatNavService = injector.get<ChatNavigationService>();
await chatNavService.navigateToAdChat(
context: context,
adsId: adId,
receiverUserId: receiverUserId,
);
} catch (e) {
logger.e("Error navigating to ad chat: $e");
}
}
Future<void> _navigateToGeneralChat(
BuildContext context,
Map<String, dynamic> data,
) async {
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;
}
log("✅ Navigating to general chat list for request: $requestId");
navigateWithName(
context,
AppRoutes.generalChatsListForProvider,
arguments: requestId,
);
} catch (e) {
logger.e("Error navigating to general chat: $e");
}
}
Future<void> displayNotification({required String title, required String body}) async {
Future<void> displayNotification({
required String title,
required String body,
String? payload,
}) async {
const AndroidNotificationDetails androidNotificationDetails = AndroidNotificationDetails(
"0",
'mowater',
@ -105,7 +326,7 @@ class FirebaseMessagingServiceImp implements FirebaseMessagingService {
const DarwinNotificationDetails iOSPlatformChannelSpecifics = DarwinNotificationDetails(threadIdentifier: 'thread_id');
const NotificationDetails notificationDetails = NotificationDetails(android: androidNotificationDetails, iOS: iOSPlatformChannelSpecifics);
await flutterLocalNotifications.show(0, title, body, notificationDetails, payload: 'item x');
await flutterLocalNotifications.show(0, title, body, notificationDetails, payload: payload ?? 'item x');
}
}

@ -26,6 +26,7 @@ class AppEnums {
enum DashboardRouteEnum {
fromAdsPayment,
fromAdsSubmit,
fromReserveAdsPayment,
none,
}

@ -263,6 +263,54 @@ class Utils {
}
}
static Color getChipColorByReserveStatus(AdReserveStatus reserveStatus) {
switch (reserveStatus) {
case AdReserveStatus.reserved:
return MyColors.primaryColor; // Active reservation - blue/primary
case AdReserveStatus.timeOver:
return MyColors.adCancelledStatusColor; // Expired - red/orange
case AdReserveStatus.cancelledByOwner:
case AdReserveStatus.cancelledByAdmin:
return MyColors.adCancelledStatusColor; // Cancelled - red
case AdReserveStatus.dealDone:
case AdReserveStatus.fullPaymentVerified:
return MyColors.greenColor; // Completed successfully - green
case AdReserveStatus.defaultStatus:
default:
return MyColors.textColor; // Default/unknown - gray
}
}
static String getReserveStatusLabel(AdReserveStatus reserveStatus) {
switch (reserveStatus) {
case AdReserveStatus.reserved:
return LocaleKeys.reserved.tr();
case AdReserveStatus.timeOver:
return LocaleKeys.reservationTimeOver.tr();
case AdReserveStatus.cancelledByOwner:
return LocaleKeys.cancelledByOwner.tr();
case AdReserveStatus.cancelledByAdmin:
return LocaleKeys.cancelledByAdmin.tr();
case AdReserveStatus.dealDone:
return LocaleKeys.dealCompleted.tr();
case AdReserveStatus.fullPaymentVerified:
return LocaleKeys.paymentVerified.tr();
case AdReserveStatus.defaultStatus:
default:
return "";
}
}
static Color getChipColorByRequestStatus(RequestStatusEnum requestStatus) {
switch (requestStatus) {
case RequestStatusEnum.pending:

@ -295,6 +295,16 @@ class AdVM extends BaseVM {
return await adsRepo.getAllAds(isMyAds: isMyAds, adPostStatus: adPostStatus, createdByRoleEnum: createdByRoleEnum, vehicleBrandId: vehicleBrandId);
}
/// Check if the "Reserved" filter is currently selected in My Ads
bool get isReservedFilterSelected {
if (myAdsFilterOptions.isEmpty) return false;
var selectedFilter = myAdsFilterOptions.firstWhere(
(element) => element.isSelected,
orElse: () => FilterListModel(id: 0, title: "All Ads", isSelected: true),
);
return selectedFilter.id.toAdPostEnum() == AdPostStatus.reserved;
}
applyFilterOnMyAds({required AdPostStatus adPostStatusEnum}) async {
pageIndexForMyAds = 1;
hasMoreDataForMyAds = true;
@ -310,9 +320,15 @@ class AdVM extends BaseVM {
getMyAds();
return;
}
// this means if the filter is reserved ads
if (adPostStatusEnum == AdPostStatus.reserved) {
log("reserved: ${myReservedAds.length}");
// Fetch reserved ads when filter is tapped (lazy loading for consistency)
setState(ViewState.busy);
await getMyReservedAds();
setState(ViewState.idle);
log("📊 Reserved ads count: ${myReservedAds.length}");
myAdsFilteredList = myReservedAds;
for (var ad in myAdsFilteredList) {
ad.isReservedByMe = true;
@ -320,6 +336,7 @@ class AdVM extends BaseVM {
notifyListeners();
return;
}
setState(ViewState.busy);
myAdsFilteredList = await getAdsByFilter(adPostStatus: adPostStatusEnum, isMyAds: true);
setState(ViewState.idle);
@ -385,10 +402,18 @@ class AdVM extends BaseVM {
}
}
Future<void> getFavouriteAds() async {
Future<void> getFavouriteAds({
AdPostStatus? adPostStatus,
CreatedByRoleEnum? createdByRoleEnum,
int? vehicleBrandId,
}) async {
try {
setState(ViewState.busy);
favouriteAds = await adsRepo.getFavouriteAds();
favouriteAds = await adsRepo.getFavouriteAds(
adPostStatus: adPostStatus,
createdByRoleEnum: createdByRoleEnum,
vehicleBrandId: vehicleBrandId,
);
notifyListeners();
setState(ViewState.idle);
} catch (e) {
@ -433,7 +458,8 @@ class AdVM extends BaseVM {
myAdsFilteredList = myAds;
final myActiveAds = myAds.where((element) => element.adPostStatus == AdPostStatus.active).toList();
myActiveAdsForHome = myActiveAds.length >= 3 ? myActiveAds.take(3).toList() : myActiveAds;
await getMyReservedAds();
// Removed: await getMyReservedAds();
// Now lazy loads when "Reserved" filter is tapped (see applyFilterOnMyAds)
notifyListeners();
setState(ViewState.idle);
}
@ -442,12 +468,19 @@ class AdVM extends BaseVM {
try {
setState(ViewState.busy);
myReservedAds = await adsRepo.getMyReservedAds();
isFetchingLists = false;
setState(ViewState.idle);
notifyListeners(); // Notify UI of data changes
return myReservedAds;
} catch (e) {
logger.e(e.toString());
logger.e("Error fetching reserved ads: ${e.toString()}");
setState(ViewState.idle);
myReservedAds = []; // Explicitly set to empty list on error
notifyListeners(); // Notify UI of error state
// Show user-friendly error message
String errorMessage = e.toString().replaceAll('Exception: ', '');
Utils.showToast(errorMessage);
return [];
}
}
@ -538,6 +571,11 @@ class AdVM extends BaseVM {
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);
@ -2733,15 +2771,23 @@ class AdVM extends BaseVM {
bool isArabic = AppState().isArabic(context);
updateSelectionVehicleModelId(SelectionModel(selectedId: previousAdDetails!.vehicle!.model!.id!, selectedOption: previousAdDetails!.vehicle!.model!.label ?? ""));
updateSelectionVehicleModelYearId(SelectionModel(selectedId: previousAdDetails!.vehicle!.modelyear!.id!, selectedOption: previousAdDetails!.vehicle!.modelyear!.label ?? ""));
updateSelectionVehicleColorId(SelectionModel(selectedId: previousAdDetails!.vehicle!.color!.id!, selectedOption: isArabic ? previousAdDetails!.vehicle!.color!.labelN.toString() : previousAdDetails!.vehicle!.color!.label ?? ""));
updateSelectionVehicleConditionId(SelectionModel(selectedId: previousAdDetails!.vehicle!.condition!.id!, selectedOption: isArabic ? previousAdDetails!.vehicle!.condition!.labelN.toString() :previousAdDetails!.vehicle!.condition!.label ?? ""));
updateSelectionVehicleCategoryId(SelectionModel(selectedId: previousAdDetails!.vehicle!.category!.id!, selectedOption: isArabic ? previousAdDetails!.vehicle!.category!.labelN.toString() :previousAdDetails!.vehicle!.category!.label ?? ""));
updateSelectionVehicleColorId(SelectionModel(
selectedId: previousAdDetails!.vehicle!.color!.id!, selectedOption: isArabic ? previousAdDetails!.vehicle!.color!.labelN.toString() : previousAdDetails!.vehicle!.color!.label ?? ""));
updateSelectionVehicleConditionId(SelectionModel(
selectedId: previousAdDetails!.vehicle!.condition!.id!,
selectedOption: isArabic ? previousAdDetails!.vehicle!.condition!.labelN.toString() : previousAdDetails!.vehicle!.condition!.label ?? ""));
updateSelectionVehicleCategoryId(SelectionModel(
selectedId: previousAdDetails!.vehicle!.category!.id!, selectedOption: isArabic ? previousAdDetails!.vehicle!.category!.labelN.toString() : previousAdDetails!.vehicle!.category!.label ?? ""));
updateSelectionVehicleMileageId(SelectionModel(
selectedId: previousAdDetails!.vehicle!.mileage!.id!,
selectedOption: "${previousAdDetails!.vehicle!.mileage!.mileageStart} - ${previousAdDetails!.vehicle!.mileage!.mileageEnd}",
));
updateSelectionVehicleTransmissionId(SelectionModel(selectedId: previousAdDetails!.vehicle!.transmission!.id!, selectedOption:isArabic ? previousAdDetails!.vehicle!.transmission!.labelN.toString() : previousAdDetails!.vehicle!.transmission!.label ?? ""));
updateSelectionVehicleSellerTypeId(SelectionModel(selectedId: previousAdDetails!.vehicle!.sellertype!.id!, selectedOption: isArabic ? previousAdDetails!.vehicle!.sellertype!.labelN.toString() :previousAdDetails!.vehicle!.sellertype!.label ?? ""));
updateSelectionVehicleTransmissionId(SelectionModel(
selectedId: previousAdDetails!.vehicle!.transmission!.id!,
selectedOption: isArabic ? previousAdDetails!.vehicle!.transmission!.labelN.toString() : previousAdDetails!.vehicle!.transmission!.label ?? ""));
updateSelectionVehicleSellerTypeId(SelectionModel(
selectedId: previousAdDetails!.vehicle!.sellertype!.id!,
selectedOption: isArabic ? previousAdDetails!.vehicle!.sellertype!.labelN.toString() : previousAdDetails!.vehicle!.sellertype!.label ?? ""));
int indexCountry = vehicleCountries.indexWhere((element) => element.id == previousAdDetails!.vehicle!.countryID);
if (indexCountry != -1) {
updateSelectionVehicleCountryId(SelectionModel(selectedId: vehicleCountries[indexCountry].id!, selectedOption: vehicleCountries[indexCountry].countryName ?? ""));

@ -225,6 +225,28 @@ class ChatVM extends BaseVM {
int providerIndex = serviceProviderOffersList.indexWhere((element) => element.providerUserId == currentMessage.senderUserID);
if (providerIndex != -1) {
// Check if message already exists to prevent duplicates
bool messageExists = false;
if (currentMessage.id != null) {
// For messages with ID, use ID for duplicate detection
messageExists = serviceProviderOffersList[providerIndex].chatMessages!.any((msg) => msg.id == currentMessage.id);
} else {
// For messages without ID (locally created), use content-based duplicate detection
// Check if same sender, same text, same reqOfferID within short time window
messageExists = serviceProviderOffersList[providerIndex].chatMessages!.any((msg) {
return msg.senderUserID == currentMessage.senderUserID &&
msg.chatText == currentMessage.chatText &&
msg.reqOfferID == currentMessage.reqOfferID &&
msg.messageType == currentMessage.messageType;
});
}
if (messageExists) {
log("Message already exists (id: ${currentMessage.id}), skipping duplicate");
continue;
}
if (currentMessage.chatMessageTypeEnum == ChatMessageTypeEnum.offer) {
for (var chatMessage in serviceProviderOffersList[providerIndex].chatMessages!) {
if (chatMessage.chatMessageTypeEnum == ChatMessageTypeEnum.offer && chatMessage.reqOffer!.id == currentMessage.reqOffer!.id) {
@ -248,6 +270,28 @@ class ChatVM extends BaseVM {
}
log("requestIndex: $requestIndex");
if (requestIndex != -1) {
// Check if message already exists to prevent duplicates
bool messageExists = false;
if (currentMessage.id != null) {
// For messages with ID, use ID for duplicate detection
messageExists = requestsVM.myFilteredRequests[requestIndex].chatMessages.any((msg) => msg.id == currentMessage.id);
} else {
// For messages without ID (locally created), use content-based duplicate detection
// Check if same sender, same text, same reqOfferID within short time window
messageExists = requestsVM.myFilteredRequests[requestIndex].chatMessages.any((msg) {
return msg.senderUserID == currentMessage.senderUserID &&
msg.chatText == currentMessage.chatText &&
msg.reqOfferID == currentMessage.reqOfferID &&
msg.messageType == currentMessage.messageType;
});
}
if (messageExists) {
log("Message already exists (id: ${currentMessage.id}), skipping duplicate");
continue;
}
if (currentMessage.chatMessageTypeEnum == ChatMessageTypeEnum.offer) {
for (var chatMessage in requestsVM.myFilteredRequests[requestIndex].chatMessages) {
if (chatMessage.chatMessageTypeEnum == ChatMessageTypeEnum.offer && chatMessage.reqOffer!.id == currentMessage.reqOffer!.id) {
@ -256,6 +300,13 @@ class ChatVM extends BaseVM {
chatMessage.reqOffer!.requestOfferStatusEnum = currentMessage.reqOffer!.requestOfferStatusEnum;
}
}
// If this offer was accepted, update the request status to inProgress
// This ensures all other offers show "offerNoLongerAvailable" on provider side
if (currentMessage.reqOffer!.requestOfferStatusEnum == RequestOfferStatusEnum.accepted) {
log("Offer accepted, updating request status to inProgress");
requestsVM.myFilteredRequests[requestIndex].requestStatus = RequestStatusEnum.inProgress;
}
}
requestsVM.addChatMessagesInRequestsModel(msg: currentMessage, index: requestIndex);
scrollChatDown();
@ -271,6 +322,13 @@ class ChatVM extends BaseVM {
return;
}
// Clear existing listener first to prevent duplicates
try {
hubConnection!.off(SignalrConsts.receiveMessageRequestOffer);
} catch (e) {
// Ignore if no listener exists
}
hubConnection!.on(SignalrConsts.receiveMessageRequestOffer, (List<Object?>? arguments) {
logger.i(arguments);
@ -328,6 +386,7 @@ class ChatVM extends BaseVM {
hubConnection!.off(SignalrConsts.receiveMessageRequestOffer);
hubConnection!.off(SignalrConsts.receiveMessageAds);
hubConnection!.off(SignalrConsts.receiveMessageGeneral);
hubConnection!.off(SignalrConsts.requestOfferReadUpdate);
_isSubscribed = false;
logger.i("Cleared all SignalR listeners");
} catch (e) {
@ -371,8 +430,10 @@ class ChatVM extends BaseVM {
_subscribeToReceiveRequestOfferMessages();
_subscribeToReceiveAdMessages();
_subscribeToReceiveGeneralMessages();
_subscribeToRequestOfferReadUpdate();
_isSubscribed = true;
logger.i("All listeners subscribed successfully");
log("✅ All SignalR listeners subscribed | Hub: ${hubConnection!.connectionId}");
log("📍 Events: ReceiveMessageRequestOffer, ReceiveMessageAds, ReceiveMessageGeneral, RequestOfferReadUpdate");
} else {
logger.w("ViewModels not injected yet. Listeners will be subscribed after injection.");
}
@ -392,13 +453,14 @@ class ChatVM extends BaseVM {
hubConnection!.onreconnected((connectionId) async {
logger.i("onReconnected: ${connectionId.toString()}");
_retryCount = 0; // Reset on successful reconnect
// CRITICAL FIX: Re-subscribe listeners after automatic reconnect
if (_requestsVM != null && _adVM != null) {
_clearAllListeners();
_subscribeToReceiveRequestOfferMessages();
_subscribeToReceiveAdMessages();
_subscribeToReceiveGeneralMessages();
_subscribeToRequestOfferReadUpdate();
_isSubscribed = true;
logger.i("✅ Listeners re-subscribed after automatic reconnection");
} else {
@ -643,7 +705,7 @@ class ChatVM extends BaseVM {
}
}
}
isSendingMessage = false;
notifyListeners();
return true;
@ -655,7 +717,7 @@ class ChatVM extends BaseVM {
return false;
}
}
isSendingMessage = false;
notifyListeners();
return false;
@ -873,6 +935,13 @@ class ChatVM extends BaseVM {
return;
}
// Clear existing listener first to prevent duplicates
try {
hubConnection!.off(SignalrConsts.receiveMessageAds);
} catch (e) {
// Ignore if no listener exists
}
hubConnection!.on(SignalrConsts.receiveMessageAds, (List<Object?>? arguments) {
if (arguments == null || arguments.isEmpty) return;
List<ChatMessageModel> chat = [];
@ -889,6 +958,23 @@ class ChatVM extends BaseVM {
Future<void> onNewMessageReceivedForAds({required List<ChatMessageModel> messages}) async {
for (var msg in messages) {
// Check if message already exists to prevent duplicates
bool messageExists = false;
if (msg.id != null) {
// For messages with ID, use ID for duplicate detection
messageExists = currentMessagesForAds.any((existingMsg) => existingMsg.id == msg.id);
} else {
// For messages without ID (locally created), use content-based duplicate detection
messageExists = currentMessagesForAds.any((existingMsg) {
return existingMsg.senderUserID == msg.senderUserID && existingMsg.chatText == msg.chatText && existingMsg.messageType == msg.messageType;
});
}
if (messageExists) {
log("Message already exists in ads chat (id: ${msg.id}), skipping duplicate");
continue;
}
currentMessagesForAds.add(msg);
}
scrollChatDown();
@ -965,7 +1051,7 @@ class ChatVM extends BaseVM {
if (AppState().currentAppType == AppType.customer) {
} else {}
isSendingMessage = false;
notifyListeners();
return true;
@ -977,7 +1063,7 @@ class ChatVM extends BaseVM {
return false;
}
}
isSendingMessage = false;
notifyListeners();
return false;
@ -1021,6 +1107,23 @@ class ChatVM extends BaseVM {
Future<void> onNewMessageReceivedForGeneral({required List<ChatMessageModel> messages}) async {
for (var msg in messages) {
// Check if message already exists to prevent duplicates
bool messageExists = false;
if (msg.id != null) {
// For messages with ID, use ID for duplicate detection
messageExists = currentMessagesForGeneralChat.any((existingMsg) => existingMsg.id == msg.id);
} else {
// For messages without ID (locally created), use content-based duplicate detection
messageExists = currentMessagesForGeneralChat.any((existingMsg) {
return existingMsg.senderUserID == msg.senderUserID && existingMsg.chatText == msg.chatText && existingMsg.messageType == msg.messageType;
});
}
if (messageExists) {
log("Message already exists in general chat (id: ${msg.id}), skipping duplicate");
continue;
}
currentMessagesForGeneralChat.add(msg); // FIXED: Use correct list
}
scrollChatDown();
@ -1028,6 +1131,13 @@ class ChatVM extends BaseVM {
}
void _subscribeToReceiveGeneralMessages() {
// Clear existing listener first to prevent duplicates
try {
hubConnection!.off(SignalrConsts.receiveMessageGeneral);
} catch (e) {
// Ignore if no listener exists
}
// General messages don't need specific VM, just storing messages
hubConnection!.on(SignalrConsts.receiveMessageGeneral, (List<Object?>? arguments) {
if (arguments == null || arguments.isEmpty) return;
@ -1043,6 +1153,120 @@ class ChatVM extends BaseVM {
logger.i("Subscribed to General messages");
}
void _subscribeToRequestOfferReadUpdate() {
if (_requestsVM == null) {
logger.e("RequestsVM not set! Cannot subscribe to request offer read updates.");
return;
}
// Clear existing listener first to prevent duplicates
try {
hubConnection!.off(SignalrConsts.requestOfferReadUpdate);
} catch (e) {
// Ignore if no listener exists
}
hubConnection!.on(SignalrConsts.requestOfferReadUpdate, (List<Object?>? arguments) {
log("🔔 RequestOfferReadUpdate EVENT TRIGGERED! Args: $arguments");
if (arguments == null || arguments.isEmpty) {
log("⚠️ RequestOfferReadUpdate: Arguments are null or empty");
return;
}
try {
final data = arguments[0] as Map<String, dynamic>;
log("✅ Parsed RequestOfferReadUpdate data: reqOfferChatID=${data['reqOfferChatID']}, isRead=${data['isRead']}");
_handleRequestOfferReadUpdate(data);
} catch (e) {
log("❌ Error parsing RequestOfferReadUpdate: ${e.toString()}");
}
});
log("✅ Subscribed to RequestOfferReadUpdate");
}
void _handleRequestOfferReadUpdate(Map<String, dynamic> data) async {
try {
final int? messageId = data['reqOfferChatID'];
final int? reqOfferID = data['reqOfferID'];
final bool? isRead = data['isRead'];
final String? readOn = data['readOn'];
if (messageId == null) {
log("❌ RequestOfferReadUpdate: reqOfferChatID is null");
return;
}
log("📨 Processing read update: msgID=$messageId, offerID=$reqOfferID, isRead=$isRead");
// For Provider App: Reload messages from API to get updated read status and IDs
if (AppState().currentAppType == AppType.provider && _requestsVM != null) {
// Check if viewing the chat currently (currentSelectedRequest is set)
if (_requestsVM!.currentSelectedRequest != null) {
log("🔄 Reloading messages for currentSelectedRequest (ID: ${_requestsVM!.currentSelectedRequest!.id})");
await _reloadRequestMessages(
requestId: _requestsVM!.currentSelectedRequest!.id,
requestOfferId: 0, // Provider side always uses 0 (sees all offers for request)
customerId: _requestsVM!.currentSelectedRequest!.customerId ?? 0,
);
log("✅ Reloaded messages for currentSelectedRequest - read status updated");
} else {
// No current chat open, optionally reload all requests
// (In most cases, we only care about updating the currently visible chat)
log(" Read update received but no active chat open, skipping reload");
}
}
// For Customer App: Not needed (customer is the one marking as read)
if (AppState().currentAppType == AppType.customer) {
log("RequestOfferReadUpdate on customer side (ignored)");
}
} catch (e) {
log("❌ Error in _handleRequestOfferReadUpdate: ${e.toString()}");
}
}
/// Helper method to reload request messages from API without showing loading dialog
Future<void> _reloadRequestMessages({
required int requestId,
required int requestOfferId,
required int customerId,
}) async {
try {
int providerId = AppState().getUser.data!.userInfo!.providerId!;
log("🔄 Fetching messages from API: requestId=$requestId, requestOfferId=$requestOfferId");
List<ChatMessageModel> chatMessages = await chatRepo.getUsersChatMessagesForRequests(
providerId: providerId,
customerId: customerId,
requestOfferId: requestOfferId,
requestId: requestId,
);
// Find the request in myFilteredRequests and update its messages
int requestIndex = _requestsVM!.myFilteredRequests.indexWhere((req) => req.id == requestId);
if (requestIndex != -1) {
_requestsVM!.myFilteredRequests[requestIndex].chatMessages = chatMessages;
// Also update currentSelectedRequest if it's the same request
if (_requestsVM!.currentSelectedRequest != null && _requestsVM!.currentSelectedRequest!.id == requestId) {
_requestsVM!.currentSelectedRequest!.chatMessages = chatMessages;
}
log("✅ Updated ${chatMessages.length} messages for request $requestId");
notifyListeners();
} else {
log("⚠️ Request $requestId not found in myFilteredRequests");
}
} catch (e) {
log("❌ Error reloading messages: ${e.toString()}");
}
}
Future<bool> onTextMessageSendForGeneralChat({
required String receiverId,
required ChatMessageTypeEnum chatMessageType,
@ -1110,7 +1334,7 @@ class ChatVM extends BaseVM {
if (AppState().currentAppType == AppType.customer) {
} else {}
isSendingMessage = false;
notifyListeners();
return true;
@ -1122,7 +1346,7 @@ class ChatVM extends BaseVM {
return false;
}
}
isSendingMessage = false;
notifyListeners();
return false;

@ -42,26 +42,26 @@ class DashboardVmCustomer extends BaseVM with WidgetsBindingObserver {
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
logger.i("App lifecycle state changed to: $state");
log("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;
}
}

@ -1,6 +1,7 @@
import 'dart:developer';
import 'package:flutter/cupertino.dart';
import 'package:flutter/scheduler.dart';
import 'package:mc_common_app/classes/app_state.dart';
import 'package:mc_common_app/config/routes.dart';
import 'package:mc_common_app/extensions/string_extensions.dart';
@ -85,8 +86,8 @@ class PaymentVM extends ChangeNotifier {
);
// Utils.hideLoading(context); // Commented out: matching pair with showLoading
log("isPaid: ${isPaid}");
// TODO: VERIFY THIS WITH ZAHOOR
log("isPaid: $isPaid");
if (!isPaid) {
// Utils.showToast(LocaleKeys.paymentFailed.tr());
// pop(context);
@ -94,9 +95,11 @@ class PaymentVM extends ChangeNotifier {
} else {
Utils.showToast(LocaleKeys.paymentSuccessful.tr());
}
// Call onVerified callback - success methods handle their own context checks
onVerified();
} catch (e) {
log("verifyPayments: ${e.toString()}");
log("verifyPayments error: ${e.toString()}");
Utils.showToast(e.toString());
return;
}
@ -147,7 +150,7 @@ class PaymentVM extends ChangeNotifier {
context: context,
paymentTypeId: paymentTypeEnum.getIdFromPaymentTypesEnum(),
id: orderProviderSubscriptionId,
onVerified: () => onSubscriptionPaymentSuccess(context),
onVerified: () => onSubscriptionPaymentSuccess(),
);
break;
@ -173,6 +176,14 @@ class PaymentVM extends ChangeNotifier {
break;
case PaymentTypes.adReserve:
await verifyPayments(
context: context,
paymentTypeId: paymentTypeEnum.getIdFromPaymentTypesEnum(),
id: currentAdId,
onVerified: () => onAdReservePaymentSuccess(context),
);
break;
case PaymentTypes.ads:
case PaymentTypes.extendAds:
await verifyPayments(
@ -187,6 +198,18 @@ 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(
AppRoutes.dashboard,
(route) => false,
);
return;
}
if (AppState().currentAppType == AppType.provider) {
context.read<DashboardVMProvider>().onNavbarTapped(4);
} else {
@ -196,6 +219,19 @@ class PaymentVM extends ChangeNotifier {
}
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(
AppRoutes.dashboard,
(route) => false,
arguments: DashboardRouteEnum.fromAdsPayment,
);
return;
}
context.read<AdVM>().updateIsExploreAds(false);
if (AppState().currentAppType == AppType.provider) {
context.read<DashboardVMProvider>().onNavbarTapped(3);
@ -207,14 +243,68 @@ class PaymentVM extends ChangeNotifier {
navigateReplaceWithNameUntilRoute(context, AppRoutes.dashboard, arguments: DashboardRouteEnum.fromAdsPayment);
}
void onSubscriptionPaymentSuccess(BuildContext context) {
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,
);
// 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");
}
});
}
void onSubscriptionPaymentSuccess() {
log("onSubscriptionPaymentSuccess");
pop(context);
// 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);
navigateReplaceWithNameUntilRoute(context, AppRoutes.dashboard);
}
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(
AppRoutes.dashboard,
(route) => false,
);
return;
}
if (AppState().currentAppType == AppType.provider) {
context.read<DashboardVMProvider>().onNavbarTapped(1);
} else {

@ -115,7 +115,6 @@ class RequestsVM extends BaseVM {
Future<void> getRequests({bool isNeedToRebuild = false}) async {
if (isNeedToRebuild) setState(ViewState.busy);
applyFilterOnRequestsVM(requestsTypeEnum: RequestsTypeEnum.specialCarRequest);
isOfferSent = false;
setState(ViewState.idle);
}
@ -302,7 +301,6 @@ class RequestsVM extends BaseVM {
}
await getRequestsBasedOnFilters();
isOfferSent = false;
notifyListeners();
}
@ -930,6 +928,9 @@ class RequestsVM extends BaseVM {
} else if (address.isEmpty && requestTypeId.selectedId == 2) {
Utils.showToast(LocaleKeys.addValidAddress.tr());
isValid = false;
} else if (pickedVehicleImages.isEmpty) {
Utils.showToast(LocaleKeys.pleaseAddAtLeastOneImage.tr());
isValid = false;
}
return isValid;
}
@ -1220,12 +1221,6 @@ class RequestsVM extends BaseVM {
return requestImages;
}
bool isOfferSent = false;
updateIsOfferSent(var value) {
isOfferSent = value;
notifyListeners();
}
Future<void> updateOfferFromProvider({
required String message,
@ -1306,81 +1301,94 @@ class RequestsVM extends BaseVM {
required int? offerId,
}) async {
if (isSendOfferValidated()) {
final chatVM = context.read<ChatVM>();
List<MessageImageModel> images = getMessagesImageList(offerId: context.read<ChatVM>().latestOfferId);
List offerImages = [];
images.forEach((element) {
offerImages.add(element.toJson());
});
bool status = await chatVM.onOfferSendForRequest(
receiverId: receiverId,
chatMessageType: ChatMessageTypeEnum.offer,
message: message,
requestId: requestId,
offerPrice: offerPrice,
isDeliveryAvailable: isDeliveryAvailable,
offerDeliveryOption: offerDeliveryOption,
manufacturedByName: manufacturedByName,
manufacturedOn: manufacturedOn,
serviceItemName: serviceItemName,
offerImages: offerImages,
context: context,
);
log("status from onOfferSendForRequest: $status");
if (!status) {
Utils.showToast(LocaleKeys.somethingWrong.tr());
return;
}
try {
Utils.showLoading(context);
final chatVM = context.read<ChatVM>();
List<MessageImageModel> images = getMessagesImageList(offerId: context.read<ChatVM>().latestOfferId);
List offerImages = [];
images.forEach((element) {
offerImages.add(element.toJson());
});
updateIsOfferSent(true);
final senderName = AppState().getUser.data!.userInfo!.firstName;
final senderId = AppState().getUser.data!.userInfo!.userId;
resetSendOfferBottomSheet();
Navigator.pop(context);
ChatMessageModel chatMessageModel = ChatMessageModel(
isMyMessage: true,
chatText: message,
messageType: ChatMessageTypeEnum.offer.getIdFromChatMessageTypeEnum(),
senderName: senderName,
senderUserID: senderId,
receiverUserID: receiverId,
chatMessageTypeEnum: ChatMessageTypeEnum.offer,
requestID: requestModel.id,
offerStatus: RequestOfferStatusEnum.offer.getIdFromRequestOfferStatusEnum(),
reqOffer: ReqOffer(
offerStatus: RequestOfferStatusEnum.offer.getIdFromRequestOfferStatusEnum(),
requestID: requestModel.id,
price: double.parse(offerPrice),
bool status = await chatVM.onOfferSendForRequest(
receiverId: receiverId,
chatMessageType: ChatMessageTypeEnum.offer,
message: message,
requestId: requestId,
offerPrice: offerPrice,
isDeliveryAvailable: isDeliveryAvailable,
requestDeliveryOption: offerDeliveryOption,
offerDeliveryOption: offerDeliveryOption,
manufacturedByName: manufacturedByName,
manufacturedOn: manufacturedOn,
serviceItemName: serviceItemName,
requestOfferStatusEnum: RequestOfferStatusEnum.offer,
comment: message,
offerStatusText: "",
reqOfferImages: images,
),
);
offerImages: offerImages,
context: context,
);
log("status from onOfferSendForRequest: $status");
context.read<ChatVM>().onNewMessageReceivedForRequestOffer(messages: [chatMessageModel], requestsVM: this, isMyOwnOffer: true);
if (!isFromChatScreen) {
ChatViewArgumentsForRequest chatViewArgumentsForRequest = ChatViewArgumentsForRequest(
chatTypeEnum: ChatTypeEnum.requestOffer,
requestId: requestModel.id,
receiverId: requestModel.customerID,
senderId: senderId ?? "",
requestIndex: requestIndex,
providerIndex: -1,
requestModel: requestModel,
if (!status) {
Utils.hideLoading(context);
Utils.showToast(LocaleKeys.somethingWrong.tr());
return;
}
// Update the specific request's isChatted status locally for immediate UI feedback
myFilteredRequests[requestIndex].isChatted = true;
notifyListeners();
final senderName = AppState().getUser.data!.userInfo!.firstName;
final senderId = AppState().getUser.data!.userInfo!.userId;
resetSendOfferBottomSheet();
Utils.hideLoading(context);
Navigator.pop(context);
ChatMessageModel chatMessageModel = ChatMessageModel(
isMyMessage: true,
chatText: message,
messageType: ChatMessageTypeEnum.offer.getIdFromChatMessageTypeEnum(),
senderName: senderName,
senderUserID: senderId,
receiverUserID: receiverId,
chatMessageTypeEnum: ChatMessageTypeEnum.offer,
requestID: requestModel.id,
offerStatus: RequestOfferStatusEnum.offer.getIdFromRequestOfferStatusEnum(),
reqOffer: ReqOffer(
offerStatus: RequestOfferStatusEnum.offer.getIdFromRequestOfferStatusEnum(),
requestID: requestModel.id,
price: double.parse(offerPrice),
isDeliveryAvailable: isDeliveryAvailable,
requestDeliveryOption: offerDeliveryOption,
manufacturedByName: manufacturedByName,
manufacturedOn: manufacturedOn,
serviceItemName: serviceItemName,
requestOfferStatusEnum: RequestOfferStatusEnum.offer,
comment: message,
offerStatusText: "",
reqOfferImages: images,
),
);
ChatViewArguments chatViewArguments = ChatViewArguments(chatTypeEnum: ChatTypeEnum.requestOffer, chatViewArgumentsForRequest: chatViewArgumentsForRequest);
navigateWithName(context, AppRoutes.chatView, arguments: chatViewArguments);
context.read<ChatVM>().onNewMessageReceivedForRequestOffer(messages: [chatMessageModel], requestsVM: this, isMyOwnOffer: true);
if (!isFromChatScreen) {
ChatViewArgumentsForRequest chatViewArgumentsForRequest = ChatViewArgumentsForRequest(
chatTypeEnum: ChatTypeEnum.requestOffer,
requestId: requestModel.id,
receiverId: requestModel.customerID,
senderId: senderId ?? "",
requestIndex: requestIndex,
providerIndex: -1,
requestModel: requestModel,
);
ChatViewArguments chatViewArguments = ChatViewArguments(chatTypeEnum: ChatTypeEnum.requestOffer, chatViewArgumentsForRequest: chatViewArgumentsForRequest);
navigateWithName(context, AppRoutes.chatView, arguments: chatViewArguments);
}
} catch (e) {
Utils.hideLoading(context);
log(e.toString());
Utils.showToast(e.toString());
}
}
}

@ -51,7 +51,8 @@ class _AdsDetailViewState extends State<AdsDetailView> {
Future<void> onAdDetailsLoaded() async {
context.read<PaymentVM>().updateCurrentAdId(id: widget.adDetails.id!);
if ((widget.adDetails.isMyAd ?? false) && (widget.adDetails.adPostStatus == AdPostStatus.reserved)) {
// 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)) {
await adVM.getAdBankingAccountInfo(adId: widget.adDetails.id!);
}
}
@ -114,8 +115,9 @@ class _AdsDetailViewState extends State<AdsDetailView> {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: "${widget.adDetails.vehicle!.vehicleTitle} | ${isArabic ? widget.adDetails.vehicle!.color!.labelN : widget.adDetails.vehicle!.color!.label}|"
" ${widget.adDetails.id}".toText(fontSize: 16, maxLines: 2),
child: "${widget.adDetails.vehicle!.vehicleTitle} | ${isArabic ? widget.adDetails.vehicle!.color!.labelN : widget.adDetails.vehicle!.color!.label}|"
" ${widget.adDetails.id}"
.toText(fontSize: 16, maxLines: 2),
),
(widget.adDetails.vehicle!.cityName ?? "").toText(fontSize: 10, color: MyColors.lightTextColor),
],
@ -126,7 +128,7 @@ class _AdsDetailViewState extends State<AdsDetailView> {
Row(
children: [
("${LocaleKeys.vehicleModel.tr()}: ").toText(fontSize: 12, color: MyColors.lightTextColor),
"${isArabic? widget.adDetails.vehicle!.model!.labelN :widget.adDetails.vehicle!.model!.label}".toText(fontSize: 12),
"${isArabic ? widget.adDetails.vehicle!.model!.labelN : widget.adDetails.vehicle!.model!.label}".toText(fontSize: 12),
],
),
(widget.adDetails.vehicle!.countryName ?? '').toText(fontSize: 10, color: MyColors.lightTextColor),
@ -138,7 +140,6 @@ class _AdsDetailViewState extends State<AdsDetailView> {
"${widget.adDetails.vehicle!.modelyear!.label}".toText(fontSize: 12),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
@ -154,7 +155,7 @@ class _AdsDetailViewState extends State<AdsDetailView> {
Row(
children: [
("${LocaleKeys.transmission.tr()}: ").toText(fontSize: 12, color: MyColors.lightTextColor),
"${isArabic? widget.adDetails.vehicle!.transmission!.labelN :widget.adDetails.vehicle!.transmission!.label}".toText(fontSize: 12),
"${isArabic ? widget.adDetails.vehicle!.transmission!.labelN : widget.adDetails.vehicle!.transmission!.label}".toText(fontSize: 12),
],
),
Row(
@ -445,7 +446,9 @@ class _AdsDetailViewState extends State<AdsDetailView> {
@override
Widget build(BuildContext context) {
Widget actionWidget = const SizedBox();
// Show delete button ONLY for ads I created/own (not ads I reserved from others)
if ((widget.adDetails.isMyAd ?? false) &&
!(widget.adDetails.isReservedByMe ?? false) &&
(widget.adDetails.adPostStatus != AdPostStatus.reserved) &&
(widget.adDetails.adPostStatus != AdPostStatus.active) &&
(widget.adDetails.adPostStatus != AdPostStatus.pendingForPost)) {
@ -506,7 +509,8 @@ class _AdsDetailViewState extends State<AdsDetailView> {
12.height,
buildPersonalInformationCard(context: context),
],
if (widget.adDetails.isMyAd ?? false) ...[
// Show special services and dates ONLY for ads I created/own (not ads I reserved from others)
if ((widget.adDetails.isMyAd ?? false) && !(widget.adDetails.isReservedByMe ?? false)) ...[
if (widget.adDetails.specialservice != null && widget.adDetails.specialservice!.isNotEmpty) ...[
12.height,
buildSpecialServicesContainer(),
@ -537,7 +541,8 @@ class _AdsDetailViewState extends State<AdsDetailView> {
),
14.height,
],
widget.adDetails.isMyAd ?? false ? BuildAdDetailsActionButtonForMyAds(adDetailsModel: widget.adDetails) : BuildAdDetailsActionButtonForExploreAds(adDetailsModel: widget.adDetails),
// Show action buttons (mark as sold, etc.) ONLY for ads I created/own (not ads I reserved)
(widget.adDetails.isMyAd ?? false) ? BuildAdDetailsActionButtonForMyAds(adDetailsModel: widget.adDetails) : BuildAdDetailsActionButtonForExploreAds(adDetailsModel: widget.adDetails),
],
),
)
@ -567,12 +572,12 @@ class _FavoriteAdButtonState extends State<FavoriteAdButton> with SingleTickerPr
void initState() {
super.initState();
isFavorite = widget.adDetails.isAdFavorite ?? false;
_animationController = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this,
);
_scaleAnimation = Tween<double>(begin: 1.0, end: 1.3).animate(
CurvedAnimation(parent: _animationController, curve: Curves.easeInOut),
);
@ -660,13 +665,13 @@ class ShareAdButton extends StatelessWidget {
void _shareAd() {
final vehicle = adDetails.vehicle!;
// Build share message
String shareMessage = '';
// Add header
shareMessage += '${LocaleKeys.checkOutThisAd.tr()}\n\n';
// Vehicle details
shareMessage += '${vehicle.vehicleTitle ?? ""}\n';
shareMessage += '${LocaleKeys.model.tr()}: ${vehicle.modelyear?.label ?? ""}\n';
@ -676,10 +681,10 @@ class ShareAdButton extends StatelessWidget {
shareMessage += '${LocaleKeys.location.tr()}: ${vehicle.cityName}\n';
}
shareMessage += '\n';
// App download links
shareMessage += '${LocaleKeys.downloadApp.tr()}\n';
if (Platform.isAndroid) {
shareMessage += '${LocaleKeys.androidApp.tr()}: https://play.google.com/store/apps/details?id=com.mowater.app\n';
} else if (Platform.isIOS) {
@ -689,7 +694,7 @@ class ShareAdButton extends StatelessWidget {
shareMessage += '${LocaleKeys.androidApp.tr()}: https://play.google.com/store/apps/details?id=com.mowater.app\n';
shareMessage += '${LocaleKeys.iosApp.tr()}: https://apps.apple.com/app/mowater/id123456789\n';
}
// Share using share_plus
Share.share(shareMessage);
}

@ -276,7 +276,6 @@ class BuildAdDetailsActionButtonForMyAds extends StatelessWidget {
void onBookPhotographyServiceClicked(BuildContext context, {required AdDetailsModel adDetailsModel}) async {
AdVM adVM = context.read<AdVM>();
log("adDetailsModel.vehicle!.cityID: ${adDetailsModel.vehicle!.cityID}");
if (adVM.photoOfficeSelectedId.selectedId == -1) {
adVM.getPhotographyServiceScheduleListByOffices(
cityId: adDetailsModel.vehicle!.cityID ?? AppState().getUser.data!.userInfo!.cityId ?? 0,
@ -759,6 +758,9 @@ 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,
@ -794,18 +796,21 @@ class BuildAdDetailsActionButtonForMyAds extends StatelessWidget {
bool status = adVM.validateReservationCancelReason();
if (status) {
Navigator.pop(context);
// Save reason before showing confirmation dialog
final String cancelReason = adVM.reservationCancelReason;
return actionConfirmationBottomSheet(
context: context,
context: parentContext, // Use parent context
title: LocaleKeys.doWantCancelReservation.tr().toText(fontSize: 28, isBold: true, letterSpacing: -1.44),
subtitle: LocaleKeys.yourAdVisibletoBuy.tr(),
subtitle: "yourAdVisibletoBuy.".tr(),
actionButtonYes: Expanded(
child: ShowFillButton(
maxHeight: 55,
title: LocaleKeys.yes.tr(),
fontSize: 15,
onPressed: () {
Navigator.pop(context);
adVM.cancelMyAdReservation(context, adId: adDetails.id!, reason: "");
onPressed: () async {
Navigator.pop(parentContext); // Pop confirmation
// Call with parent context that's still valid
await adVM.cancelMyAdReservation(parentContext, adId: adDetails.id!, reason: cancelReason);
},
),
),
@ -1040,12 +1045,16 @@ 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);
case AdPostStatus.active:
return markAsSoldAction(context);
case AdPostStatus.reserved:
// adPostStatus=reserved is set in getMyReservedAds()
// adReserveStatus tells us the detailed status (reserved, timeOver, dealDone, etc.)
AdVM adVM = context.watch<AdVM>();
if (adVM.state == ViewState.busy) {
return const CircularProgressIndicator();

@ -275,7 +275,18 @@ class _AdCardState extends State<AdCard> {
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
if (isAdsFragment && widget.adDetails.adPostStatus! == AdPostStatus.sold && widget.adDetails.statuslabel != null && widget.adDetails.statuslabel!.isNotEmpty) ...[
// When viewing reserved ads filter, show ONLY reserve status badge
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!)
),
]
// 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!)),
] 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!)),

@ -10,6 +10,7 @@ import 'package:mc_common_app/views/advertisement/components/ads_list_widget.dar
import 'package:mc_common_app/widgets/common_widgets/app_bar.dart';
import 'package:mc_common_app/widgets/common_widgets/filters_list.dart';
import 'package:mc_common_app/widgets/extensions/extensions_widget.dart';
import 'package:mc_common_app/widgets/shimmer/home_shimmer_widgets.dart';
import 'package:provider/provider.dart';
import 'package:easy_localization/easy_localization.dart';
@ -36,12 +37,13 @@ class _FavouriteAdsViewState extends State<FavouriteAdsView> {
super.initState();
}
List<AdDetailsModel> getFilteredAds(AdVM adVM) {
if (selectedBrandId == 0) {
return adVM.favouriteAds;
}
return adVM.favouriteAds.where((ad) => ad.vehicle?.model?.vehicleBrandID == selectedBrandId).toList();
}
// Removed client-side filtering - now using API filter
// List<AdDetailsModel> getFilteredAds(AdVM adVM) {
// if (selectedBrandId == 0) {
// return adVM.favouriteAds;
// }
// return adVM.favouriteAds.where((ad) => ad.vehicle?.model?.vehicleBrandID == selectedBrandId).toList();
// }
@override
Widget build(BuildContext context) {
@ -58,7 +60,7 @@ class _FavouriteAdsViewState extends State<FavouriteAdsView> {
FiltersList(
showImages: true,
filterList: adVM.exploreAdsFilterOptions,
onFilterTapped: (index, selectedFilterId) {
onFilterTapped: (index, selectedFilterId) async {
setState(() {
selectedBrandId = selectedFilterId;
});
@ -67,6 +69,11 @@ class _FavouriteAdsViewState extends State<FavouriteAdsView> {
filter.isSelected = false;
}
adVM.exploreAdsFilterOptions[index].isSelected = true;
// Call API with filter (0 = all brands)
await adVM.getFavouriteAds(
vehicleBrandId: selectedFilterId == 0 ? null : selectedFilterId,
);
},
needLeftPadding: false,
).paddingOnly(left: 21),
@ -75,11 +82,13 @@ class _FavouriteAdsViewState extends State<FavouriteAdsView> {
Expanded(
child: RefreshIndicator(
onRefresh: () async {
await adVM.getFavouriteAds();
await adVM.getFavouriteAds(
vehicleBrandId: selectedBrandId == 0 ? null : selectedBrandId,
);
},
child: adVM.state == ViewState.busy
? const Center(child: CircularProgressIndicator())
: getFilteredAds(adVM).isEmpty
? const AdsListShimmer(itemCount: 5)
: adVM.favouriteAds.isEmpty
? Center(
child: Text(
LocaleKeys.noFavouriteAds.tr(),
@ -90,7 +99,7 @@ class _FavouriteAdsViewState extends State<FavouriteAdsView> {
isAdsFragment: false,
isDraftAds: false,
shouldShowAdStatus: false,
adsList: getFilteredAds(adVM),
adsList: adVM.favouriteAds, // Use API-filtered list directly
hasMoreData: false,
),
),

@ -780,19 +780,36 @@ class _ChatMessageCustomWidgetState extends State<ChatMessageCustomWidget> {
Widget buildFreeTextDetailsInMessage({required ChatMessageTypeEnum chatMessageTypeEnum}) {
return Row(
children: [
if (chatMessageTypeEnum == ChatMessageTypeEnum.offer && (widget.chatMessageModel.isMyMessage == true) && (widget.chatMessageModel.isRead ?? false)) ...[
Row(
children: [
const Icon(Icons.remove_red_eye_outlined, size: 12, color: MyColors.lightTextColor),
4.width,
LocaleKeys.viewed.tr().toText(fontSize: 10, color: MyColors.lightTextColor, fontWeight: MyFonts.Medium),
],
),
] else if (AppState().currentAppType == AppType.provider &&
// For Provider: Show status and edit button
if (AppState().currentAppType == AppType.provider &&
chatMessageTypeEnum == ChatMessageTypeEnum.offer &&
widget.chatMessageModel.reqOffer!.requestOfferStatusEnum == RequestOfferStatusEnum.offer &&
(widget.chatMessageModel.isMyMessage == true && widget.chatMessageModel.reqOffer != null && widget.chatMessageModel.reqOffer!.id != null)) ...[
MyAssets.icEdit.buildSvg(color: MyColors.white, height: 15).onPress(() => onOfferEditIconPressed()),
widget.chatMessageModel.isMyMessage == true &&
widget.chatMessageModel.reqOffer != null &&
widget.chatMessageModel.reqOffer!.requestOfferStatusEnum == RequestOfferStatusEnum.offer) ...[
// Show read status if message is read
if (widget.chatMessageModel.isRead ?? false) ...[
Row(
children: [
const Icon(Icons.done_all, size: 14, color: Colors.blue),
4.width,
LocaleKeys.read.tr().toText(fontSize: 10, color: Colors.blue, fontWeight: MyFonts.Medium),
8.width,
],
),
]
// Show edit button if message is NOT read
else ...[
Row(
children: [
const Icon(Icons.check, size: 14, color: MyColors.lightTextColor),
4.width,
LocaleKeys.sent.tr().toText(fontSize: 10, color: MyColors.lightTextColor, fontWeight: MyFonts.Medium),
8.width,
MyAssets.icEdit.buildSvg(color: MyColors.white, height: 15).onPress(() => onOfferEditIconPressed()),
8.width,
],
),
],
],
Expanded(
child: Directionality(

@ -46,21 +46,23 @@ class AdsFragment extends StatelessWidget {
isRemoveBackButton: true,
actions: [
Padding(
padding: EdgeInsets.only(top: adVM.adsFiltersCounter > 0 ? 20 : 0, right: 21),
padding: EdgeInsets.only(
top: adVM.adsFiltersCounter > 0 ? 20 : 0, right: 21),
child: Badge(
isLabelVisible: adVM.adsFiltersCounter > 0,
largeSize: 15,
smallSize: 20,
backgroundColor: MyColors.darkPrimaryColor,
label: Text('${adVM.adsFiltersCounter}'),
child: MyAssets.searchIcon.buildSvg(),
child: MyAssets.searchIcon.buildSvg(height: 22, width: 22),
),
).onPress(() {
navigateWithName(context, AppRoutes.adsFilterView);
})
],
).toViewOnly(context, onTap: () {
navigateWithName(context, AppRoutes.loginWithPassword, arguments: false);
navigateWithName(context, AppRoutes.loginWithPassword,
arguments: false);
}),
body: Stack(
children: [
@ -71,7 +73,8 @@ class AdsFragment extends StatelessWidget {
children: [
16.height,
Consumer(
builder: (BuildContext context, AdVM adVM, Widget? child) {
builder:
(BuildContext context, AdVM adVM, Widget? child) {
return Column(
children: [
Row(
@ -82,7 +85,9 @@ class AdsFragment extends StatelessWidget {
maxHeight: 55,
fontSize: 15,
title: LocaleKeys.exploreAds.tr(),
txtColor: adVM.isExploreAdsTapped ? MyColors.white : MyColors.darkTextColor,
txtColor: adVM.isExploreAdsTapped
? MyColors.white
: MyColors.darkTextColor,
onPressed: () {
log("accessToken: ${AppState().getUser.data!.accessToken}");
log("AppState().getUser.data!.userInfo!.userId;: ${AppState().getUser.data!.userInfo!.userId}");
@ -98,7 +103,9 @@ class AdsFragment extends StatelessWidget {
Expanded(
child: ShowFillButton(
isFilled: !adVM.isExploreAdsTapped,
txtColor: !adVM.isExploreAdsTapped ? MyColors.white : MyColors.darkTextColor,
txtColor: !adVM.isExploreAdsTapped
? MyColors.white
: MyColors.darkTextColor,
maxHeight: 55,
fontSize: 15,
title: LocaleKeys.myAds.tr(),
@ -106,7 +113,9 @@ class AdsFragment extends StatelessWidget {
adVM.updateIsExploreAds(false);
},
).toViewOnly(context, onTap: () {
navigateWithName(context, AppRoutes.loginWithPassword, arguments: false);
navigateWithName(
context, AppRoutes.loginWithPassword,
arguments: false);
}),
),
],
@ -118,7 +127,8 @@ class AdsFragment extends StatelessWidget {
showImages: true,
filterList: adVM.exploreAdsFilterOptions,
onFilterTapped: (index, selectedFilterId) {
adVM.applyFilterOnExploreAds(vehicleBrandId: selectedFilterId);
adVM.applyFilterOnExploreAds(
vehicleBrandId: selectedFilterId);
},
needLeftPadding: false,
).paddingOnly(left: 21),
@ -128,7 +138,9 @@ class AdsFragment extends StatelessWidget {
FiltersList(
filterList: adVM.myAdsFilterOptions,
onFilterTapped: (index, selectedFilterId) {
adVM.applyFilterOnMyAds(adPostStatusEnum: selectedFilterId.toAdPostEnum());
adVM.applyFilterOnMyAds(
adPostStatusEnum:
selectedFilterId.toAdPostEnum());
},
needLeftPadding: false,
).paddingOnly(left: 21),
@ -142,11 +154,19 @@ class AdsFragment extends StatelessWidget {
child: RefreshIndicator(
onRefresh: () async {
if (adVM.isExploreAdsTapped) {
int vehicleBrandId = adVM.exploreAdsFilterOptions.firstWhere((element) => element.isSelected).id;
adVM.applyFilterOnExploreAds(vehicleBrandId: vehicleBrandId);
int vehicleBrandId = adVM.exploreAdsFilterOptions
.firstWhere((element) => element.isSelected)
.id;
adVM.applyFilterOnExploreAds(
vehicleBrandId: vehicleBrandId);
} else {
AdPostStatus adPostStatusEnum = adVM.myAdsFilterOptions.firstWhere((element) => element.isSelected).id.toAdPostEnum();
adVM.applyFilterOnMyAds(adPostStatusEnum: adPostStatusEnum);
AdPostStatus adPostStatusEnum = adVM
.myAdsFilterOptions
.firstWhere((element) => element.isSelected)
.id
.toAdPostEnum();
adVM.applyFilterOnMyAds(
adPostStatusEnum: adPostStatusEnum);
}
},
child: adVM.state == ViewState.busy
@ -156,14 +176,22 @@ class AdsFragment extends StatelessWidget {
isDraftAds: false,
shouldShowAdStatus: !adVM.isExploreAdsTapped,
adsList: getAdsList(adVM),
hasMoreData: adVM.isExploreAdsTapped ? adVM.hasMoreDataForExploreAds : adVM.hasMoreDataForMyAds,
hasMoreData: adVM.isExploreAdsTapped
? adVM.hasMoreDataForExploreAds
: adVM.hasMoreDataForMyAds,
onFetchMoreAds: () async {
AdPostStatus adPostStatusEnum = adVM.myAdsFilterOptions.firstWhere((element) => element.isSelected).id.toAdPostEnum();
AdPostStatus adPostStatusEnum = adVM
.myAdsFilterOptions
.firstWhere(
(element) => element.isSelected)
.id
.toAdPostEnum();
if (adVM.isExploreAdsTapped) {
await adVM.fetchMoreExploreAds();
} else {
await adVM.fetchMoreMyAds(adsStatus: adPostStatusEnum);
await adVM.fetchMoreMyAds(
adsStatus: adPostStatusEnum);
}
},
),
@ -187,7 +215,8 @@ class AdsFragment extends StatelessWidget {
color: Colors.black.withOpacity(0.3),
child: const Center(
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(MyColors.darkPrimaryColor),
valueColor: AlwaysStoppedAnimation<Color>(
MyColors.darkPrimaryColor),
),
),
),
@ -197,30 +226,38 @@ class AdsFragment extends StatelessWidget {
onPressed: () async {
// Await API calls with loading state
await adVM.getVehicleAdsDuration();
// Ensure vehicle types are loaded
if (adVM.vehicleTypes.isEmpty) {
await adVM.getVehicleTypes();
}
if (AppState().userType != UserType.providerDealer) {
if (adVM.vehicleAdDurationId.selectedId == -1 && adVM.vehicleAdsDurations.isNotEmpty) {
if (adVM.vehicleAdDurationId.selectedId == -1 &&
adVM.vehicleAdsDurations.isNotEmpty) {
SelectionModel selection = SelectionModel(
selectedId: adVM.vehicleAdsDurations.first.id ?? 0,
selectedOption: ("${adVM.vehicleAdsDurations.first.days} ${LocaleKeys.daysVar.tr()}"),
selectedOption:
("${adVM.vehicleAdsDurations.first.days} ${LocaleKeys.daysVar.tr()}"),
itemPrice: adVM.vehicleAdsDurations.first.price!.toString(),
currency: adVM.vehicleAdsDurations.first.currency!.toString(),
currency:
adVM.vehicleAdsDurations.first.currency!.toString(),
);
adVM.updateVehicleAdDurationId(selection);
}
}
navigateWithName(context, AppRoutes.selectAdTypeView, arguments: [injector.get<AppState>().currentAppType == AppType.provider, false, -1]);
navigateWithName(context, AppRoutes.selectAdTypeView, arguments: [
injector.get<AppState>().currentAppType == AppType.provider,
false,
-1
]);
},
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);
}),
);
},

@ -29,7 +29,9 @@ 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(
@ -41,7 +43,10 @@ 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);
}
@ -69,7 +74,8 @@ class MyRequestsFragment extends StatelessWidget {
return false;
}
if (requestStatusEnum == RequestStatusEnum.pending || requestStatusEnum == RequestStatusEnum.submitted) {
if (requestStatusEnum == RequestStatusEnum.pending ||
requestStatusEnum == RequestStatusEnum.submitted) {
return true;
}
return false;
@ -77,7 +83,8 @@ class MyRequestsFragment extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Consumer(builder: (BuildContext context, RequestsVM requestsVM, Widget? child) {
return Consumer(
builder: (BuildContext context, RequestsVM requestsVM, Widget? child) {
return Scaffold(
appBar: CustomAppBar(
title: LocaleKeys.manageRequests.tr(),
@ -86,7 +93,9 @@ 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,
@ -96,21 +105,24 @@ 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,
smallSize: 20,
backgroundColor: MyColors.darkPrimaryColor,
label: Text('${requestsVM.requestsFiltersCounter}'),
child: MyAssets.searchIcon.buildSvg(),
child: MyAssets.searchIcon.buildSvg(height: 22, width: 22),
),
).onPress(() {
navigateWithName(context, AppRoutes.requestsFilterView);
}),
],
).toViewOnly(context, onTap: () {
navigateWithName(context, AppRoutes.loginWithPassword, arguments: false);
navigateWithName(context, AppRoutes.loginWithPassword,
arguments: false);
}),
body: Container(
color: MyColors.backgroundColor,
@ -122,7 +134,8 @@ 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
@ -131,27 +144,31 @@ 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,
@ -170,43 +187,76 @@ 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);
}),
);
},
@ -214,7 +264,8 @@ 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),
),
))
],
@ -222,14 +273,16 @@ 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,
);

@ -303,7 +303,7 @@ class RequestDetailPage extends StatelessWidget {
appBar: CustomAppBar(
title: LocaleKeys.requestDetail.tr(),
actions: [
(context.read<RequestsVM>().isOfferSent || requestDetailPageArguments.requestModel.isChatted)
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(
@ -332,7 +332,7 @@ class RequestDetailPage extends StatelessWidget {
allPading: 12,
margin: const EdgeInsets.only(top: 21, right: 21, left: 21),
),
if (!context.read<RequestsVM>().isOfferSent && !requestDetailPageArguments.requestModel.isChatted) ...[
if (!requestDetailPageArguments.requestModel.isChatted) ...[
ShowFillButton(
maxWidth: double.infinity,
margin: const EdgeInsets.all(15),

@ -47,11 +47,13 @@ class _ChangePasswordPageState extends State<ChangePasswordPage> {
// 12.height,
TxtField(
hint: LocaleKeys.enterOldPassword.tr(),
isPasswordEnabled: true,
onChanged: (v) => currentPassword = v,
),
12.height,
TxtField(
hint: LocaleKeys.enterNewPassword.tr(),
isPasswordEnabled: true,
onChanged: (v) => newPassword = v,
),
20.height,

@ -21,6 +21,8 @@ class ConfirmNewPasswordPage extends StatefulWidget {
class _ConfirmNewPasswordPageState extends State<ConfirmNewPasswordPage> {
String newPassword = "";
String confirmPassword = '';
bool _obscureNewPassword = true;
bool _obscureConfirmPassword = true;
@override
Widget build(BuildContext context) {
@ -42,7 +44,6 @@ class _ConfirmNewPasswordPageState extends State<ConfirmNewPasswordPage> {
12.height,
TextFormField(
decoration: InputDecoration(
// hintText: "Enter New Password",
hintText: LocaleKeys.enterNewPassword.tr(),
hintStyle: const TextStyle(color: Colors.grey),
border: const OutlineInputBorder(
@ -50,14 +51,24 @@ class _ConfirmNewPasswordPageState extends State<ConfirmNewPasswordPage> {
Radius.circular(5.0),
),
),
suffixIcon: IconButton(
icon: Icon(
_obscureNewPassword ? Icons.visibility_off : Icons.visibility,
color: Colors.grey,
),
onPressed: () {
setState(() {
_obscureNewPassword = !_obscureNewPassword;
});
},
),
),
obscureText: true,
obscureText: _obscureNewPassword,
onChanged: (v) => newPassword = v,
),
12.height,
TextFormField(
decoration: InputDecoration(
// hintText: "Confirm Password",
hintText: LocaleKeys.confirmPassword.tr(),
hintStyle: const TextStyle(color: Colors.grey),
border: const OutlineInputBorder(
@ -65,8 +76,19 @@ class _ConfirmNewPasswordPageState extends State<ConfirmNewPasswordPage> {
Radius.circular(5.0),
),
),
suffixIcon: IconButton(
icon: Icon(
_obscureConfirmPassword ? Icons.visibility_off : Icons.visibility,
color: Colors.grey,
),
onPressed: () {
setState(() {
_obscureConfirmPassword = !_obscureConfirmPassword;
});
},
),
),
obscureText: true,
obscureText: _obscureConfirmPassword,
onChanged: (v) => confirmPassword = v,
),
40.height,

@ -36,8 +36,6 @@ class FiltersList extends StatelessWidget {
itemCount: filterList.length,
scrollDirection: Axis.horizontal,
itemBuilder: (BuildContext context, int index) {
print("==================================================");
print(filterList[index].title);
return InkWell(
onTap: () {
onFilterTapped(index, filterList[index].id);
@ -68,16 +66,12 @@ class FiltersList extends StatelessWidget {
],
],
// Text displayed in the chip
(isArabic
? (filterList[index].titleN ?? filterList[index].title)
: filterList[index].title)
.tr()
.toText(
// filterList[index].title.tr().toText(
fontSize: 13,
letterSpacing: -0.14,
color: filterList[index].isSelected ? MyColors.white : null,
),
(isArabic ? (filterList[index].titleN ?? filterList[index].title) : filterList[index].title).toText(
// filterList[index].title.tr().toText(
fontSize: 13,
letterSpacing: -0.14,
color: filterList[index].isSelected ? MyColors.white : null,
),
],
),
),

@ -8,7 +8,7 @@ import 'package:mc_common_app/utils/utils.dart';
import 'package:mc_common_app/widgets/extensions/extensions_widget.dart';
import 'package:sizer/sizer.dart';
class TxtField extends StatelessWidget {
class TxtField extends StatefulWidget {
String? value;
String? hint;
String? lable;
@ -70,20 +70,26 @@ class TxtField extends StatelessWidget {
this.isNeedLabelOnTop = true,
});
@override
State<TxtField> createState() => _TxtFieldState();
}
class _TxtFieldState extends State<TxtField> {
TextEditingController controller = TextEditingController();
bool _obscurePassword = true;
@override
Widget build(BuildContext context) {
controller.text = value ?? "";
controller.text = widget.value ?? "";
controller.selection = TextSelection.fromPosition(TextPosition(offset: controller.text.length));
return Column(
children: [
if (isNeedLabelOnTop && hint != null) ...[
if (widget.isNeedLabelOnTop && widget.hint != null) ...[
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
(hint ?? "").toText(
(widget.hint ?? "").toText(
color: borderColor,
fontSize: 13,
fontWeight: MyFonts.Medium,
@ -93,10 +99,10 @@ class TxtField extends StatelessWidget {
4.height,
],
InkWell(
onTap: isNeedClickAll == false
onTap: widget.isNeedClickAll == false
? null
: () {
onTap!();
widget.onTap!();
},
customBorder: Utils.inkWellCorner(),
child: Row(
@ -104,70 +110,80 @@ class TxtField extends StatelessWidget {
Expanded(
child: Container(
decoration: BoxDecoration(
color: isBackgroundEnabled ? MyColors.textFieldColor : Colors.white,
color: widget.isBackgroundEnabled ? MyColors.textFieldColor : Colors.white,
borderRadius: const BorderRadius.all(Radius.circular(0)),
),
child: TextField(
style: const TextStyle(color: MyColors.darkTextColor, fontSize: 15, fontWeight: MyFonts.Medium),
textInputAction: isSearchBar ? TextInputAction.search : null,
keyboardType: keyboardType,
textInputAction: widget.isSearchBar ? TextInputAction.search : null,
keyboardType: widget.keyboardType,
autofocus: false,
controller: controller,
enabled: isNeedClickAll == true ? false : true,
maxLines: maxLines,
enabled: widget.isNeedClickAll == true ? false : true,
maxLines: widget.maxLines,
onTap: () {},
obscureText: isPasswordEnabled ?? false,
onChanged: onChanged,
onSubmitted: onSubmitted,
inputFormatters: allowOnlyOneDigit && numbersOnly
obscureText: widget.isPasswordEnabled == true ? _obscurePassword : false,
onChanged: widget.onChanged,
onSubmitted: widget.onSubmitted,
inputFormatters: widget.allowOnlyOneDigit && widget.numbersOnly
? <TextInputFormatter>[FilteringTextInputFormatter.allow(RegExp('[0-9]')), LengthLimitingTextInputFormatter(1)]
: numbersOnly
: widget.numbersOnly
? <TextInputFormatter>[FilteringTextInputFormatter.allow(RegExp('[0-9]'))]
: [],
decoration: InputDecoration(
labelText: lable,
labelText: widget.lable,
alignLabelWithHint: true,
fillColor: Colors.white,
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: isNeedBorder ? MyColors.darkPrimaryColor : Colors.transparent, width: isNeedBorder ? 2.0 : 0),
borderSide: BorderSide(color: widget.isNeedBorder ? MyColors.darkPrimaryColor : Colors.transparent, width: widget.isNeedBorder ? 2.0 : 0),
borderRadius: BorderRadius.circular(0.0),
),
enabledBorder: OutlineInputBorder(
// borderSide: BorderSide(color: MyColors.textFieldColor, width: isNeedBorder ? 1.0 : 0),
borderSide: BorderSide(color: isNeedBorder ? MyColors.darkPrimaryColor : Colors.transparent, width: isNeedBorder ? 2.0 : 0),
borderSide: BorderSide(color: widget.isNeedBorder ? MyColors.darkPrimaryColor : Colors.transparent, width: widget.isNeedBorder ? 2.0 : 0),
borderRadius: BorderRadius.circular(0.0),
),
disabledBorder: OutlineInputBorder(
// borderSide: BorderSide(color: MyColors.textFieldColor, width: isNeedBorder ? 1.0 : 0),
borderSide: BorderSide(color: isNeedBorder ? MyColors.darkPrimaryColor : Colors.transparent, width: isNeedBorder ? 2.0 : 0),
borderSide: BorderSide(color: widget.isNeedBorder ? MyColors.darkPrimaryColor : Colors.transparent, width: widget.isNeedBorder ? 2.0 : 0),
borderRadius: BorderRadius.circular(0.0),
),
suffixIcon: postfixData != null
? InkWell(
onTap: onPostFixPressed,
child: Icon(postfixData, color: postFixDataColor),
suffixIcon: widget.isPasswordEnabled == true
? IconButton(
icon: Icon(
_obscurePassword ? Icons.visibility_off : Icons.visibility,
color: Colors.grey,
),
onPressed: () {
setState(() {
_obscurePassword = !_obscurePassword;
});
},
)
: postfixWidget,
prefixIcon: prefixData != null ? Icon(prefixData, color: borderColor) : preFixWidget,
: widget.postfixData != null
? InkWell(
onTap: widget.onPostFixPressed,
child: Icon(widget.postfixData, color: widget.postFixDataColor),
)
: widget.postfixWidget,
prefixIcon: widget.prefixData != null ? Icon(widget.prefixData, color: borderColor) : widget.preFixWidget,
labelStyle: const TextStyle(color: borderColor, fontSize: 13, fontWeight: MyFonts.Medium),
hintStyle: const TextStyle(color: borderColor, fontSize: 13, fontWeight: MyFonts.Medium),
hintText: isNeedLabelOnTop ? null : hint ?? "",
contentPadding: prefixData == null
hintText: widget.isNeedLabelOnTop ? null : widget.hint ?? "",
contentPadding: widget.prefixData == null
? EdgeInsets.only(
left: 12,
right: 12,
top: maxLines != null ? 12 : 0,
bottom: maxLines != null ? 12 : 0,
top: widget.maxLines != null ? 12 : 0,
bottom: widget.maxLines != null ? 12 : 0,
)
: EdgeInsets.zero,
),
),
),
),
if (isNeedFilterButton) 8.width,
if (isNeedFilterButton)
if (widget.isNeedFilterButton) 8.width,
if (widget.isNeedFilterButton)
InkWell(
onTap: isNeedClickAll
onTap: widget.isNeedClickAll
? null
: () {
controller.clear();
@ -177,30 +193,26 @@ class TxtField extends StatelessWidget {
height: 55,
child: Card(
color: accentColor,
// margin: EdgeInsets.all(4),
// shape: cardRadius(0),
child: Icon(
postfixData ?? Icons.filter_alt,
color: postFixDataColor,
widget.postfixData ?? Icons.filter_alt,
color: widget.postFixDataColor,
),
),
),
),
if (isButtonEnable)
if (widget.isButtonEnable)
Material(
child: InkWell(
onTap: onTap,
onTap: widget.onTap,
customBorder: Utils.inkWellCorner(),
child: SizedBox(
height: 55,
child: Card(
color: accentColor,
// margin: EdgeInsets.all(4),
// shape: cardRadius(0),
child: Center(
child: Padding(
padding: const EdgeInsets.only(left: 12, right: 12),
child: (buttonTitle ?? "Search").toText(
child: (widget.buttonTitle ?? "Search").toText(
color: Colors.white,
fontSize: 16,
letterSpacing: -0.64,
@ -214,11 +226,11 @@ class TxtField extends StatelessWidget {
],
),
),
if (errorValue != "")
if (widget.errorValue != "")
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
errorValue.toText(fontSize: 12, color: Colors.red),
widget.errorValue.toText(fontSize: 12, color: Colors.red),
],
).paddingOnly(right: 10),
],

@ -149,10 +149,10 @@ packages:
dependency: transitive
description:
name: characters
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
url: "https://pub.dev"
source: hosted
version: "1.4.0"
version: "1.4.1"
clock:
dependency: transitive
description:
@ -873,26 +873,26 @@ packages:
dependency: transitive
description:
name: matcher
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
url: "https://pub.dev"
source: hosted
version: "0.12.17"
version: "0.12.19"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
url: "https://pub.dev"
source: hosted
version: "0.11.1"
version: "0.13.0"
meta:
dependency: transitive
description:
name: meta
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
url: "https://pub.dev"
source: hosted
version: "1.16.0"
version: "1.18.0"
mime:
dependency: transitive
description:
@ -1342,10 +1342,10 @@ packages:
dependency: transitive
description:
name: test_api
sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00"
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
url: "https://pub.dev"
source: hosted
version: "0.7.6"
version: "0.7.11"
timezone:
dependency: transitive
description:
@ -1539,5 +1539,5 @@ packages:
source: hosted
version: "6.5.0"
sdks:
dart: ">=3.8.0-0 <4.0.0"
dart: ">=3.10.0-0 <4.0.0"
flutter: ">=3.35.0"

Loading…
Cancel
Save