Merge branch 'faiz_development_common' into fatima_dev

# Conflicts:
#	lib/view_models/requests_view_model.dart
#	lib/views/advertisement/components/ads_list_widget.dart
#	lib/views/requests/widget/request_item.dart
#	lib/widgets/common_widgets/filters_list.dart
#	pubspec.lock
pull/7/head
CS 1 month ago
commit b8625db7e4

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

@ -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",
@ -899,5 +902,6 @@
"downloadApp": "Download MoWater App:",
"androidApp": "Android",
"iosApp": "iOS",
"pleaseWait": "Please wait..."
"pleaseWait": "Please wait...",
"numberOfShipping": "Number of Shipping"
}

@ -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";
@ -380,6 +381,9 @@ class MyAssets {
static String whatsAppIcon = "${assetPath}icons/whatsapp_icon.svg";
static const String arrowRight = "${assetPath}icons/ic_arrow_right.svg";
static const String brokenImage = "${assetPath}images/broken_image.png";
// Tones
static const String engineStartSound = "${assetPath}tones/engine_start_tone.mp3";
}
RegExp numReg = RegExp(r".*[0-9].*");
@ -419,6 +423,7 @@ class SignalrConsts {
// Requests
static String receiveMessageRequestOffer = "ReceiveMessageRequestOffer";
static String sendMessageRequestOffer = "SendMessageRequestOffer";
static String requestOfferReadUpdate = "RequestOfferReadUpdate";
// Ads
static String sendMessageAds = "SendMessageAds";
@ -471,4 +476,3 @@ class GuestConsts {
},
);
}

@ -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": "قيد المراجعة",
@ -917,7 +920,8 @@ class CodegenLoader extends AssetLoader{
"downloadApp": "تحميل تطبيق موّاتر:",
"androidApp": "أندرويد",
"iosApp": "آيفون",
"pleaseWait": "يرجى الانتظار..."
"pleaseWait": "يرجى الانتظار...",
"numberOfShipping": "عدد الشحنات"
};
static const Map<String,dynamic> _en_US = {
"firstTimeLogIn": "First Time Log In",
@ -1641,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.",
@ -1753,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",
@ -1820,7 +1827,8 @@ static const Map<String,dynamic> _en_US = {
"downloadApp": "Download MoWater App:",
"androidApp": "Android",
"iosApp": "iOS",
"pleaseWait": "Please wait..."
"pleaseWait": "Please wait...",
"numberOfShipping": "Number of Shipping"
};
static const Map<String, Map<String,dynamic>> mapLocales = {"ar_SA": _ar_SA, "en_US": _en_US};
}

@ -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';
@ -883,5 +886,5 @@ abstract class LocaleKeys {
static const androidApp = 'androidApp';
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'];

@ -15,6 +15,7 @@ class ShippingRequestModel {
String? customerName;
int? vehicleTypeID;
String? vehicleType;
String? numberOfShipping;
ShippingRequestModel({
this.id,
@ -28,6 +29,7 @@ class ShippingRequestModel {
this.customerName,
this.vehicleTypeID,
this.vehicleType,
this.numberOfShipping,
});
ShippingRequestModel.fromJsonShipping(Map<String, dynamic> json, bool isForShipping) {
@ -45,6 +47,7 @@ class ShippingRequestModel {
customerName = json['customerName'];
vehicleTypeID = json['vehicleTypeID'];
vehicleType = json['vehicleType'];
numberOfShipping = json['numberOfShipping'];
}
}

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

@ -18,9 +18,9 @@ abstract class ShippingRepo {
Future<List<ShippingRequestModel>> getShippingRequestListByStatus({ShippingRequestStatusEnum? shippingStatusEnum, int? requestId});
Future<GenericRespModel> updateShippingRequestStatus({required ShippingRequestStatusEnum shippingStatusEnum, required int shippingRequestId, String? comment});
Future<GenericRespModel> updateShippingRequestStatus({required ShippingRequestStatusEnum shippingStatusEnum, required int shippingRequestId, String? comment, String? numberOfShipping});
Future<GenericRespModel> updateSelfPickupRequestStatus({required SelfPickupRequestStatusEnum selfPickupStatusEnum, required int shippingRequestId, String? comment});
Future<GenericRespModel> updateSelfPickupRequestStatus({required SelfPickupRequestStatusEnum selfPickupStatusEnum, required int shippingRequestId, String? comment, String? numberOfShipping});
}
class ShippingRepoImp extends ShippingRepo {
@ -78,12 +78,13 @@ class ShippingRepoImp extends ShippingRepo {
}
@override
Future<GenericRespModel> updateShippingRequestStatus({required ShippingRequestStatusEnum shippingStatusEnum, required int shippingRequestId, String? comment}) async {
Future<GenericRespModel> updateShippingRequestStatus({required ShippingRequestStatusEnum shippingStatusEnum, required int shippingRequestId, String? comment, String? numberOfShipping}) async {
String token = appState.getUser.data!.accessToken ?? "";
Map<String, String> queryParameters = {
"id": "$shippingRequestId",
"shippingStatus": "${shippingStatusEnum.getIdFromShippingStatusEnum()}",
"comment": comment ?? "",
"numberOfShipping": numberOfShipping ?? "",
};
GenericRespModel genericRespModel = await apiClient.postJsonForObject(
@ -101,12 +102,13 @@ class ShippingRepoImp extends ShippingRepo {
}
@override
Future<GenericRespModel> updateSelfPickupRequestStatus({required SelfPickupRequestStatusEnum selfPickupStatusEnum, required int shippingRequestId, String? comment}) async {
Future<GenericRespModel> updateSelfPickupRequestStatus({required SelfPickupRequestStatusEnum selfPickupStatusEnum, required int shippingRequestId, String? comment, String? numberOfShipping}) async {
String token = appState.getUser.data!.accessToken ?? "";
Map<String, String> queryParameters = {
"id": "$shippingRequestId",
"selfPickUpStatus": "${selfPickupStatusEnum.getIdFromSelfPickupStatusEnum()}",
"comment": comment ?? "",
"numberOfShipping": numberOfShipping ?? "",
};
GenericRespModel genericRespModel = await apiClient.postJsonForObject(

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

@ -238,6 +238,7 @@ class AdVM extends BaseVM {
bool hasMoreDataForExploreAds = true;
bool hasMoreDataForMyAds = true;
bool isLoadingMore = false;
bool isLoadingAdsDuration = false;
Future<void> fetchMoreExploreAds() async {
if (isLoadingMore) return;
@ -295,6 +296,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 +321,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 +337,7 @@ class AdVM extends BaseVM {
notifyListeners();
return;
}
setState(ViewState.busy);
myAdsFilteredList = await getAdsByFilter(adPostStatus: adPostStatusEnum, isMyAds: true);
setState(ViewState.idle);
@ -385,10 +403,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 +459,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 +469,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 [];
}
}
@ -458,9 +492,18 @@ class AdVM extends BaseVM {
}
Future<void> getVehicleAdsDuration() async {
final countryId = AppState().getUser.data!.userInfo!.countryId;
vehicleAdsDurations = await adsRepo.getAdsDuration(countryId: countryId);
isLoadingAdsDuration = true;
notifyListeners();
try {
final countryId = AppState().getUser.data!.userInfo!.countryId;
vehicleAdsDurations = await adsRepo.getAdsDuration(countryId: countryId);
isLoadingAdsDuration = false;
notifyListeners();
} catch (e) {
isLoadingAdsDuration = false;
notifyListeners();
rethrow;
}
}
Future<void> getVehicleAdsSpecialServices({required int cityId, required int countryId, required int specialServiceType}) async {
@ -529,6 +572,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);
@ -2724,15 +2772,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 ?? ""));

@ -35,6 +35,20 @@ class ChatVM extends BaseVM {
HubConnection? hubConnection;
bool _isSubscribed = false; // Track if listeners are subscribed
// Store ViewModels to avoid context dependency
RequestsVM? _requestsVM;
AdVM? _adVM;
// Track if a message is currently being sent (prevents duplicate sends)
bool isSendingMessage = false;
// Method to inject ViewModels (called once from Dashboard)
void setViewModels({required RequestsVM requestsVM, required AdVM adVM}) {
_requestsVM = requestsVM;
_adVM = adVM;
logger.i("ViewModels injected into ChatVM");
}
String chatMessageText = "";
updateChatMessageText(String value) {
@ -211,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) {
@ -234,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) {
@ -242,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();
@ -251,7 +316,19 @@ class ChatVM extends BaseVM {
notifyListeners();
}
void _subscribeToReceiveRequestOfferMessages(BuildContext context) {
void _subscribeToReceiveRequestOfferMessages() {
if (_requestsVM == null) {
logger.e("RequestsVM not set! Cannot subscribe to request offer messages.");
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);
@ -261,9 +338,10 @@ class ChatVM extends BaseVM {
final chatMessage = ChatMessageModel.fromJson(message as Map<String, dynamic>, isForReqOfferImagesURLs: true);
chat.add(chatMessage);
}
onNewMessageReceivedForRequestOffer(messages: chat, requestsVM: context.read<RequestsVM>());
onNewMessageReceivedForRequestOffer(messages: chat, requestsVM: _requestsVM!);
// Utils.showToast(arguments.toString());
});
logger.i("Subscribed to RequestOffer messages");
}
Future<bool> markMessagesAsRead(BuildContext context, List<int> messageIds, ChatTypeEnum chatTypeEnum) async {
@ -308,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) {
@ -346,13 +425,17 @@ class ChatVM extends BaseVM {
// Clear old listeners before subscribing (prevents duplicates)
_clearAllListeners();
// Subscribe to messages (only if context is available for first-time connection)
// For reconnections, we'll re-subscribe without context
if (context != null) {
_subscribeToReceiveRequestOfferMessages(context);
_subscribeToReceiveAdMessages(context);
_subscribeToReceiveGeneralMessages(context);
// Subscribe to messages (no context needed anymore!)
if (_requestsVM != null && _adVM != null) {
_subscribeToReceiveRequestOfferMessages();
_subscribeToReceiveAdMessages();
_subscribeToReceiveGeneralMessages();
_subscribeToRequestOfferReadUpdate();
_isSubscribed = true;
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.");
}
// Configure handlers
@ -367,11 +450,22 @@ class ChatVM extends BaseVM {
_isSubscribed = false;
});
hubConnection!.onreconnected((connectionId) {
hubConnection!.onreconnected((connectionId) async {
logger.i("onReconnected: ${connectionId.toString()}");
// Re-subscribe after automatic reconnect (SignalR's own reconnect, not ours)
// This is safe because we clear listeners in onreconnecting
_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 {
logger.e("❌ Cannot re-subscribe: ViewModels not set!");
}
});
} catch (e) {
logger.e("Error during hub connection setup: ${e.toString()}");
@ -527,83 +621,112 @@ class ChatVM extends BaseVM {
}) async {
if (chatMessageType == ChatMessageTypeEnum.freeText && message.isEmpty) return false;
// Ensure connection is established
if (hubConnection == null || hubConnection!.state != HubConnectionState.connected) {
logger.i("Hub Not Connected!! Attempting to connect...");
await buildHubConnection(context: context);
// ✅ Prevent duplicate sends
if (isSendingMessage) {
logger.w("Message already being sent, ignoring duplicate tap");
return false;
}
// Wait a bit for connection to establish
await Future.delayed(const Duration(milliseconds: 500));
isSendingMessage = true;
notifyListeners();
// Check again after attempt
try {
// Ensure connection is established
if (hubConnection == null || hubConnection!.state != HubConnectionState.connected) {
Utils.showToast("Failed to connect. Please try again.");
return false;
}
}
if (hubConnection!.state == HubConnectionState.connected) {
final userId = AppState().getUser.data!.userInfo!.userId.toString();
final name = AppState().getUser.data!.userInfo!.firstName.toString();
List<Map<String, dynamic>> requestImagesMap = [];
List<MessageImageModel> messageImages = getMessagesImageList();
if (messageImages.isNotEmpty && chatMessageType == ChatMessageTypeEnum.image) {
for (var element in messageImages) {
requestImagesMap.add(element.toJson());
logger.i("Hub Not Connected!! Attempting to connect...");
await buildHubConnection(context: context);
// Wait a bit for connection to establish
await Future.delayed(const Duration(milliseconds: 500));
// Check again after attempt
if (hubConnection == null || hubConnection!.state != HubConnectionState.connected) {
Utils.showToast("Failed to connect. Please try again.");
isSendingMessage = false;
notifyListeners();
return false;
}
}
final obj = <String, dynamic>{
"ReceiverUserID": receiverId,
"SenderUserID": userId,
"MessageType": chatMessageType.getIdFromChatMessageTypeEnum(),
"ChatText": chatMessageType == ChatMessageTypeEnum.image ? "${messageImages.length} Image(s)" : message,
"RequestID": requestId,
"ReqOfferID": latestOfferId,
};
if (chatMessageType == ChatMessageTypeEnum.image) {
obj.addAll({"ReqOfferImages": requestImagesMap});
}
logger.i(obj);
try {
await hubConnection!.invoke(
SignalrConsts.sendMessageRequestOffer,
args: <Object>[obj],
);
ChatMessageModel chatMessageModel = ChatMessageModel(
messageType: chatMessageType.getIdFromChatMessageTypeEnum(),
chatText: message,
isMyMessage: true,
requestID: requestId,
senderName: name,
senderUserID: userId,
chatMessageTypeEnum: chatMessageType,
receiverUserID: receiverId,
messageImages: messageImages,
);
if (AppState().currentAppType == AppType.customer) {
int providerIndex = serviceProviderOffersList.indexWhere((element) => element.providerUserId == receiverId);
if (providerIndex != -1) {
serviceProviderOffersList[providerIndex].chatMessages!.add(chatMessageModel);
if (hubConnection!.state == HubConnectionState.connected) {
final userId = AppState().getUser.data!.userInfo!.userId.toString();
final name = AppState().getUser.data!.userInfo!.firstName.toString();
List<Map<String, dynamic>> requestImagesMap = [];
List<MessageImageModel> messageImages = getMessagesImageList();
if (messageImages.isNotEmpty && chatMessageType == ChatMessageTypeEnum.image) {
for (var element in messageImages) {
requestImagesMap.add(element.toJson());
}
} else {
int providerIndex = context.read<RequestsVM>().myFilteredRequests.indexWhere((element) => (element.customerUserID == receiverId && element.id == requestId));
log("providerIndex2:$providerIndex");
if (providerIndex != -1) {
context.read<RequestsVM>().addChatMessagesInRequestsModel(msg: chatMessageModel, index: providerIndex);
}
final obj = <String, dynamic>{
"ReceiverUserID": receiverId,
"SenderUserID": userId,
"MessageType": chatMessageType.getIdFromChatMessageTypeEnum(),
"ChatText": chatMessageType == ChatMessageTypeEnum.image ? "${messageImages.length} Image(s)" : message,
"RequestID": requestId,
"ReqOfferID": latestOfferId,
};
if (chatMessageType == ChatMessageTypeEnum.image) {
obj.addAll({"ReqOfferImages": requestImagesMap});
}
logger.i(obj);
try {
await hubConnection!.invoke(
SignalrConsts.sendMessageRequestOffer,
args: <Object>[obj],
);
ChatMessageModel chatMessageModel = ChatMessageModel(
messageType: chatMessageType.getIdFromChatMessageTypeEnum(),
chatText: message,
isMyMessage: true,
requestID: requestId,
senderName: name,
senderUserID: userId,
chatMessageTypeEnum: chatMessageType,
receiverUserID: receiverId,
messageImages: messageImages,
);
if (AppState().currentAppType == AppType.customer) {
int providerIndex = serviceProviderOffersList.indexWhere((element) => element.providerUserId == receiverId);
if (providerIndex != -1) {
serviceProviderOffersList[providerIndex].chatMessages!.add(chatMessageModel);
}
} else {
// ✅ Use stored RequestsVM instead of context.read
if (_requestsVM != null) {
int providerIndex = _requestsVM!.myFilteredRequests.indexWhere((element) => (element.customerUserID == receiverId && element.id == requestId));
log("providerIndex2:$providerIndex");
if (providerIndex != -1) {
_requestsVM!.addChatMessagesInRequestsModel(msg: chatMessageModel, index: providerIndex);
}
}
}
isSendingMessage = false;
notifyListeners();
return true;
} catch (e) {
logger.e("error in invoking SendMessage: ${e.toString()}");
Utils.showToast("Failed to send message");
isSendingMessage = false;
notifyListeners();
return false;
}
return true;
} catch (e) {
logger.e("error in invoking SendMessage: ${e.toString()}");
Utils.showToast("Failed to send message");
return false;
}
isSendingMessage = false;
notifyListeners();
return false;
} catch (e) {
logger.e("Unexpected error in onMessageSendForRequest: ${e.toString()}");
isSendingMessage = false;
notifyListeners();
return false;
}
return false;
}
List<ServiceProvidersOffers> serviceProviderOffersList = [];
@ -806,7 +929,19 @@ class ChatVM extends BaseVM {
}
}
void _subscribeToReceiveAdMessages(BuildContext context) {
void _subscribeToReceiveAdMessages() {
if (_adVM == null) {
logger.e("AdVM not set! Cannot subscribe to ad messages.");
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 = [];
@ -818,10 +953,28 @@ class ChatVM extends BaseVM {
logger.i(arguments);
// Utils.showToast(arguments.toString());
});
logger.i("Subscribed to Ads messages");
}
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();
@ -837,63 +990,89 @@ class ChatVM extends BaseVM {
}) async {
if (message.isEmpty) return false;
// Ensure connection is established
if (hubConnection == null || hubConnection!.state != HubConnectionState.connected) {
logger.i("Hub Not Connected!! Attempting to connect...");
await buildHubConnection(context: context);
// ✅ Prevent duplicate sends
if (isSendingMessage) {
logger.w("Message already being sent, ignoring duplicate tap");
return false;
}
// Wait a bit for connection to establish
await Future.delayed(const Duration(milliseconds: 500));
isSendingMessage = true;
notifyListeners();
// Check again after attempt
try {
// Ensure connection is established
if (hubConnection == null || hubConnection!.state != HubConnectionState.connected) {
Utils.showToast("Failed to connect. Please try again.");
return false;
logger.i("Hub Not Connected!! Attempting to connect...");
await buildHubConnection(context: context);
// Wait a bit for connection to establish
await Future.delayed(const Duration(milliseconds: 500));
// Check again after attempt
if (hubConnection == null || hubConnection!.state != HubConnectionState.connected) {
Utils.showToast("Failed to connect. Please try again.");
isSendingMessage = false;
notifyListeners();
return false;
}
}
}
if (hubConnection!.state == HubConnectionState.connected) {
final userId = AppState().getUser.data!.userInfo!.userId.toString();
final name = AppState().getUser.data!.userInfo!.firstName.toString();
final obj = <String, dynamic>{
"ReceiverUserID": receiverId,
// "SenderUserID": userId,
"MessageType": chatMessageType.getIdFromChatMessageTypeEnum(),
"ChatText": message,
"AdsID": adId,
};
logger.i("$obj");
try {
await hubConnection!.invoke(
SignalrConsts.sendMessageAds,
args: <Object>[obj],
);
ChatMessageModel chatMessageModel = ChatMessageModel(
messageType: chatMessageType.getIdFromChatMessageTypeEnum(),
chatText: message,
isMyMessage: true,
senderName: name,
senderUserID: userId,
chatMessageTypeEnum: ChatMessageTypeEnum.freeText,
receiverUserID: receiverId,
);
currentMessagesForAds.add(chatMessageModel);
notifyListeners();
if (AppState().currentAppType == AppType.customer) {
} else {}
return true;
} catch (e) {
logger.e("error in invoking SendMessage: ${e.toString()}");
Utils.showToast("Failed to send message");
return false;
if (hubConnection!.state == HubConnectionState.connected) {
final userId = AppState().getUser.data!.userInfo!.userId.toString();
final name = AppState().getUser.data!.userInfo!.firstName.toString();
final obj = <String, dynamic>{
"ReceiverUserID": receiverId,
// "SenderUserID": userId,
"MessageType": chatMessageType.getIdFromChatMessageTypeEnum(),
"ChatText": message,
"AdsID": adId,
};
logger.i("$obj");
try {
await hubConnection!.invoke(
SignalrConsts.sendMessageAds,
args: <Object>[obj],
);
ChatMessageModel chatMessageModel = ChatMessageModel(
messageType: chatMessageType.getIdFromChatMessageTypeEnum(),
chatText: message,
isMyMessage: true,
senderName: name,
senderUserID: userId,
chatMessageTypeEnum: ChatMessageTypeEnum.freeText,
receiverUserID: receiverId,
);
currentMessagesForAds.add(chatMessageModel);
notifyListeners();
if (AppState().currentAppType == AppType.customer) {
} else {}
isSendingMessage = false;
notifyListeners();
return true;
} catch (e) {
logger.e("error in invoking SendMessage: ${e.toString()}");
Utils.showToast("Failed to send message");
isSendingMessage = false;
notifyListeners();
return false;
}
}
isSendingMessage = false;
notifyListeners();
return false;
} catch (e) {
logger.e("Unexpected error in onTextMessageSendForAds: ${e.toString()}");
isSendingMessage = false;
notifyListeners();
return false;
}
return false;
}
// ========================General ==================
@ -928,13 +1107,38 @@ 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();
notifyListeners();
}
void _subscribeToReceiveGeneralMessages(BuildContext context) {
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;
List<ChatMessageModel> chat = [];
@ -946,6 +1150,121 @@ class ChatVM extends BaseVM {
logger.i(arguments);
// Utils.showToast(arguments.toString());
});
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({
@ -957,59 +1276,85 @@ class ChatVM extends BaseVM {
}) async {
if (message.isEmpty) return false;
// Ensure connection is established
if (hubConnection == null || hubConnection!.state != HubConnectionState.connected) {
logger.i("Hub Not Connected!! Attempting to connect...");
await buildHubConnection(context: context);
// ✅ Prevent duplicate sends
if (isSendingMessage) {
logger.w("Message already being sent, ignoring duplicate tap");
return false;
}
// Wait a bit for connection to establish
await Future.delayed(const Duration(milliseconds: 500));
isSendingMessage = true;
notifyListeners();
// Check again after attempt
try {
// Ensure connection is established
if (hubConnection == null || hubConnection!.state != HubConnectionState.connected) {
Utils.showToast("Failed to connect. Please try again.");
return false;
logger.i("Hub Not Connected!! Attempting to connect...");
await buildHubConnection(context: context);
// Wait a bit for connection to establish
await Future.delayed(const Duration(milliseconds: 500));
// Check again after attempt
if (hubConnection == null || hubConnection!.state != HubConnectionState.connected) {
Utils.showToast("Failed to connect. Please try again.");
isSendingMessage = false;
notifyListeners();
return false;
}
}
}
if (hubConnection!.state == HubConnectionState.connected) {
final userId = AppState().getUser.data!.userInfo!.userId.toString();
final name = AppState().getUser.data!.userInfo!.firstName.toString();
final obj = <String, dynamic>{
"ReceiverUserID": receiverId,
"MessageType": chatMessageType.getIdFromChatMessageTypeEnum(),
"ChatText": message,
};
logger.i("$obj");
try {
await hubConnection!.invoke(
SignalrConsts.sendMessageGeneral,
args: <Object>[obj],
);
ChatMessageModel chatMessageModel = ChatMessageModel(
messageType: chatMessageType.getIdFromChatMessageTypeEnum(),
chatText: message,
isMyMessage: true,
senderName: name,
senderUserID: userId,
chatMessageTypeEnum: ChatMessageTypeEnum.freeText,
receiverUserID: receiverId,
);
currentMessagesForGeneralChat.add(chatMessageModel);
notifyListeners();
if (AppState().currentAppType == AppType.customer) {
} else {}
return true;
} catch (e) {
logger.e("error in invoking SendMessage: ${e.toString()}");
Utils.showToast("Failed to send message");
return false;
if (hubConnection!.state == HubConnectionState.connected) {
final userId = AppState().getUser.data!.userInfo!.userId.toString();
final name = AppState().getUser.data!.userInfo!.firstName.toString();
final obj = <String, dynamic>{
"ReceiverUserID": receiverId,
"MessageType": chatMessageType.getIdFromChatMessageTypeEnum(),
"ChatText": message,
};
logger.i("$obj");
try {
await hubConnection!.invoke(
SignalrConsts.sendMessageGeneral,
args: <Object>[obj],
);
ChatMessageModel chatMessageModel = ChatMessageModel(
messageType: chatMessageType.getIdFromChatMessageTypeEnum(),
chatText: message,
isMyMessage: true,
senderName: name,
senderUserID: userId,
chatMessageTypeEnum: ChatMessageTypeEnum.freeText,
receiverUserID: receiverId,
);
currentMessagesForGeneralChat.add(chatMessageModel);
notifyListeners();
if (AppState().currentAppType == AppType.customer) {
} else {}
isSendingMessage = false;
notifyListeners();
return true;
} catch (e) {
logger.e("error in invoking SendMessage: ${e.toString()}");
Utils.showToast("Failed to send message");
isSendingMessage = false;
notifyListeners();
return false;
}
}
isSendingMessage = false;
notifyListeners();
return false;
} catch (e) {
logger.e("Unexpected error in onTextMessageSendForGeneralChat: ${e.toString()}");
isSendingMessage = false;
notifyListeners();
return false;
}
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;
}
}
@ -107,6 +107,9 @@ class DashboardVmCustomer extends BaseVM with WidgetsBindingObserver {
RequestsVM requestsVM = Provider.of<RequestsVM>(context, listen: false);
ChatVM chatVM = Provider.of<ChatVM>(context, listen: false);
// ✅ Inject ViewModels into ChatVM (only needs to be done once, but safe to repeat)
chatVM.setViewModels(requestsVM: requestsVM, adVM: adVM);
await appointmentsVM.populateBranchesFilterList();
await appointmentsVM.populateAppointmentsFilterList();
await adVM.populateAdsFilterList();

@ -122,6 +122,10 @@ class DashboardVMProvider extends BaseVM with WidgetsBindingObserver {
final serviceVM = context.read<ServiceVM>();
final adVM = context.read<AdVM>();
final subscriptionsVM = context.read<SubscriptionsVM>();
// ✅ Inject ViewModels into ChatVM (only needs to be done once, but safe to repeat)
chatVM.setViewModels(requestsVM: requestsVM, adVM: adVM);
await requestsVM.populateDataForRequestsFilter();
await requestsVM.populateDeliveryOptionEnums();
await appointmentVM.populateAppointmentsFilterList();

@ -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,7 +86,8 @@ class PaymentVM extends ChangeNotifier {
);
// Utils.hideLoading(context); // Commented out: matching pair with showLoading
// TODO: VERIFY THIS WITH ZAHOOR
log("isPaid: $isPaid");
if (!isPaid) {
// Utils.showToast(LocaleKeys.paymentFailed.tr());
// pop(context);
@ -93,8 +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 error: ${e.toString()}");
Utils.showToast(e.toString());
return;
}
@ -145,7 +150,7 @@ class PaymentVM extends ChangeNotifier {
context: context,
paymentTypeId: paymentTypeEnum.getIdFromPaymentTypesEnum(),
id: orderProviderSubscriptionId,
onVerified: () => onSubscriptionPaymentSuccess(context),
onVerified: () => onSubscriptionPaymentSuccess(),
);
break;
@ -171,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(
@ -185,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 {
@ -194,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);
@ -205,13 +243,68 @@ class PaymentVM extends ChangeNotifier {
navigateReplaceWithNameUntilRoute(context, AppRoutes.dashboard, arguments: DashboardRouteEnum.fromAdsPayment);
}
void onSubscriptionPaymentSuccess(BuildContext context) {
pop(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");
// 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);
}
@ -216,7 +215,7 @@ class RequestsVM extends BaseVM {
if (acceptedRequestsTypeFilterOptions.isEmpty) {
for (int i = 0; i < myRequestsTypeEnum.length; i++) {
acceptedRequestsTypeFilterOptions.add(FilterListModel(title: myRequestsTypeEnum[i].enumValueStr, titleN: myRequestsTypeEnum[i].enumValueStrN, isSelected: false, id: myRequestsTypeEnum[i].enumValue));
acceptedRequestsTypeFilterOptions.add(FilterListModel(title: myRequestsTypeEnum[i].enumValueStr, isSelected: false, id: myRequestsTypeEnum[i].enumValue));
}
acceptedRequestsTypeFilterOptions[0].isSelected = true;
}
@ -302,7 +301,6 @@ class RequestsVM extends BaseVM {
}
await getRequestsBasedOnFilters();
isOfferSent = false;
notifyListeners();
}
@ -383,16 +381,16 @@ class RequestsVM extends BaseVM {
if (requestsTypeFilterOptions.isEmpty || acceptedRequestsTypeFilterOptions.isEmpty) {
for (int i = 0; i < myRequestsTypeEnum.length; i++) {
requestsTypeFilterOptions.add(FilterListModel(title: myRequestsTypeEnum[i].enumValueStr,titleN: myRequestsTypeEnum[i].enumValueStrN, isSelected: false, id: myRequestsTypeEnum[i].enumValue));
acceptedRequestsTypeFilterOptions.add(FilterListModel(title: myRequestsTypeEnum[i].enumValueStr, titleN: myRequestsTypeEnum[i].enumValueStrN, isSelected: false, id: myRequestsTypeEnum[i].enumValue));
requestsTypeFilterOptions.add(FilterListModel(title: myRequestsTypeEnum[i].enumValueStr, isSelected: false, id: myRequestsTypeEnum[i].enumValue));
acceptedRequestsTypeFilterOptions.add(FilterListModel(title: myRequestsTypeEnum[i].enumValueStr, isSelected: false, id: myRequestsTypeEnum[i].enumValue));
}
}
// Initialize spare parts condition filters (All, New, Used)
if (sparePartsConditionFilterOptions.isEmpty) {
sparePartsConditionFilterOptions.add(FilterListModel(title: "All", titleN: "الكل", isSelected: true, id: 3)); // All
sparePartsConditionFilterOptions.add(FilterListModel(title: "New Spare Part", titleN: "قطعة غيار جديدة", isSelected: false, id: 1)); // New
sparePartsConditionFilterOptions.add(FilterListModel(title: "Used Spare Part",titleN: "قطعة غيار مستعملة", isSelected: false, id: 2)); // Used
sparePartsConditionFilterOptions.add(FilterListModel(title: "New", titleN: " جديدة", isSelected: false, id: 1)); // New
sparePartsConditionFilterOptions.add(FilterListModel(title: "Used",titleN: " مستعملة", isSelected: false, id: 2)); // Used
}
if (vehicleBrands.isEmpty) {
@ -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");
try {
Utils.showLoading(context);
if (!status) {
Utils.showToast(LocaleKeys.somethingWrong.tr());
return;
}
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());
}
}
}

@ -30,11 +30,16 @@ class ShippingManagementVM extends BaseVM {
List<FilterListModel> shippingRequestStatusesList = [];
String requestStatusComments = "";
String numberOfShipping = "";
void updateRequestStatusComments(var value) {
requestStatusComments = value;
}
void updateNumberOfShipping(var value) {
numberOfShipping = value;
}
bool isSelfPickupTapped = false;
void updateIsSelfPickupTapped(var value) {
@ -55,6 +60,7 @@ class ShippingManagementVM extends BaseVM {
}
selfPickupRequestsFilterOptions[0].isSelected = true;
requestStatusComments = "";
numberOfShipping = "";
}
Future<void> populateSelfPickupRequestFilterList() async {
@ -174,10 +180,16 @@ class ShippingManagementVM extends BaseVM {
Future<bool> onUpdateShippingStatusTapped({required BuildContext context, required ShippingRequestStatusEnum shippingStatusEnum, required int shippingRequestId}) async {
Utils.showLoading(context);
try {
GenericRespModel? genericRespModel = await shippingRepo.updateShippingRequestStatus(shippingStatusEnum: shippingStatusEnum, shippingRequestId: shippingRequestId, comment: requestStatusComments);
GenericRespModel? genericRespModel = await shippingRepo.updateShippingRequestStatus(
shippingStatusEnum: shippingStatusEnum,
shippingRequestId: shippingRequestId,
comment: requestStatusComments,
numberOfShipping: numberOfShipping.isEmpty ? null : numberOfShipping,
);
Utils.showToast(genericRespModel.message.toString());
Utils.hideLoading(context);
requestStatusComments = "";
numberOfShipping = "";
return genericRespModel.messageStatus == 1;
} catch (e) {
logger.i(e.toString());
@ -190,11 +202,16 @@ class ShippingManagementVM extends BaseVM {
Future<bool> onUpdateSelfPickupStatusTapped({required BuildContext context, required SelfPickupRequestStatusEnum selfPickupRequestStatusEnum, required int shippingRequestId}) async {
Utils.showLoading(context);
try {
GenericRespModel? genericRespModel =
await shippingRepo.updateSelfPickupRequestStatus(selfPickupStatusEnum: selfPickupRequestStatusEnum, shippingRequestId: shippingRequestId, comment: requestStatusComments);
GenericRespModel? genericRespModel = await shippingRepo.updateSelfPickupRequestStatus(
selfPickupStatusEnum: selfPickupRequestStatusEnum,
shippingRequestId: shippingRequestId,
comment: requestStatusComments,
numberOfShipping: numberOfShipping.isEmpty ? null : numberOfShipping,
);
Utils.showToast(genericRespModel.message.toString());
Utils.hideLoading(context);
requestStatusComments = "";
numberOfShipping = "";
return genericRespModel.messageStatus == 1;
} catch (e) {
logger.i(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),
),
(isArabic ? widget.adDetails.vehicle!.cityNameN ?? "" : 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),
],
),
(isArabic ? widget.adDetails.vehicle!.countryNameN?? '' : 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: [
@ -156,7 +157,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(
@ -461,7 +462,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)) {
@ -522,7 +525,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(),
@ -553,7 +557,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),
],
),
)
@ -583,12 +588,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),
);
@ -676,13 +681,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';
@ -692,10 +697,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) {
@ -705,7 +710,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();

@ -174,8 +174,8 @@ class _AdCardState extends State<AdCard> {
children: [
ClipRRect(
child: SizedBox(
width: 100,
height: 100,
width: 80,
height: 80,
child: hasMultipleImages
? PageView.builder(
controller: _pageController,
@ -275,31 +275,21 @@ class _AdCardState extends State<AdCard> {
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
// if (isAdsFragment && adDetails.adPostStatus! == AdPostStatus.sold && adDetails.statuslabel != null && adDetails.statuslabel!.isNotEmpty) ...[
// Utils.statusContainerChip(text: adDetails.statuslabel ?? "", chipColor: Utils.getChipColorByAdStatus(adDetails.adPostStatus!)),
// ] else if (isAdsFragment && !context.read<AdVM>().isExploreAdsTapped && adDetails.statuslabel != null && adDetails.statuslabel!.isNotEmpty) ...[
// Utils.statusContainerChip(text: adDetails.statuslabel ?? "", chipColor: Utils.getChipColorByAdStatus(adDetails.adPostStatus!)),
// ],
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: isArabic
? (widget.adDetails.statuslabelN ?? "")
: (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: isArabic
? (widget.adDetails.statuslabelN ?? "")
: (widget.adDetails.statuslabel ?? ""),
chipColor: Utils.getChipColorByAdStatus(widget.adDetails.adPostStatus!),
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!)),
],
// Show Accident Free chip for explore ads when isAccidentFree is true
if (isAdsFragment && context.read<AdVM>().isExploreAdsTapped && widget.adDetails.vehicle?.isAccidentFree == true) ...[

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

@ -307,25 +307,34 @@ class _ChatViewState extends State<ChatView> {
],
Expanded(
flex: 1,
child: const Icon(Icons.send_rounded, color: MyColors.darkPrimaryColor, size: 30).onPress(
() async {
ChatMessageTypeEnum chatMessageTypeEnum = ChatMessageTypeEnum.freeText;
child: chatVM.isSendingMessage
? const SizedBox(
width: 30,
height: 30,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(MyColors.darkPrimaryColor),
),
)
: const Icon(Icons.send_rounded, color: MyColors.darkPrimaryColor, size: 30).onPress(
() async {
ChatMessageTypeEnum chatMessageTypeEnum = ChatMessageTypeEnum.freeText;
if (chatVM.pickedImagesForMessage.isNotEmpty) {
chatMessageTypeEnum = ChatMessageTypeEnum.image;
}
final status = await onMessageSend(chatMessageType: chatMessageTypeEnum);
if (chatVM.pickedImagesForMessage.isNotEmpty) {
chatMessageTypeEnum = ChatMessageTypeEnum.image;
}
final status = await onMessageSend(chatMessageType: chatMessageTypeEnum);
if (status) {
chatVM.scrollChatDown();
if (chatMessageTypeEnum == ChatMessageTypeEnum.freeText) {
chatVM.clearChatMessageText();
} else if (chatMessageTypeEnum == ChatMessageTypeEnum.image) {
chatVM.clearPickedImagesForMessage();
}
}
},
),
if (status) {
chatVM.scrollChatDown();
if (chatMessageTypeEnum == ChatMessageTypeEnum.freeText) {
chatVM.clearChatMessageText();
} else if (chatMessageTypeEnum == ChatMessageTypeEnum.image) {
chatVM.clearPickedImagesForMessage();
}
}
},
),
),
],
],

@ -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,158 +46,218 @@ 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: SizedBox(
width: double.infinity,
height: double.infinity,
child: Column(
children: [
16.height,
Consumer(
builder: (BuildContext context, AdVM adVM, Widget? child) {
return Column(
children: [
Row(
body: Stack(
children: [
SizedBox(
width: double.infinity,
height: double.infinity,
child: Column(
children: [
16.height,
Consumer(
builder:
(BuildContext context, AdVM adVM, Widget? child) {
return Column(
children: [
Expanded(
child: ShowFillButton(
isFilled: adVM.isExploreAdsTapped,
maxHeight: 55,
fontSize: 15,
title: LocaleKeys.exploreAds.tr(),
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}");
Row(
children: [
Expanded(
child: ShowFillButton(
isFilled: adVM.isExploreAdsTapped,
maxHeight: 55,
fontSize: 15,
title: LocaleKeys.exploreAds.tr(),
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}");
if (adVM.myAds.isEmpty) {
adVM.getMyAds();
if (adVM.myAds.isEmpty) {
adVM.getMyAds();
}
adVM.updateIsExploreAds(true);
},
),
),
12.width,
Expanded(
child: ShowFillButton(
isFilled: !adVM.isExploreAdsTapped,
txtColor: !adVM.isExploreAdsTapped
? MyColors.white
: MyColors.darkTextColor,
maxHeight: 55,
fontSize: 15,
title: LocaleKeys.myAds.tr(),
onPressed: () {
adVM.updateIsExploreAds(false);
},
).toViewOnly(context, onTap: () {
navigateWithName(
context, AppRoutes.loginWithPassword,
arguments: false);
}),
),
],
).horPaddingMain(),
if (adVM.isExploreAdsTapped) ...[
if (adVM.adsFiltersCounter == 0) ...[
16.height,
FiltersList(
showImages: true,
filterList: adVM.exploreAdsFilterOptions,
onFilterTapped: (index, selectedFilterId) {
adVM.applyFilterOnExploreAds(
vehicleBrandId: selectedFilterId);
},
needLeftPadding: false,
).paddingOnly(left: 21),
]
] else ...[
16.height,
FiltersList(
filterList: adVM.myAdsFilterOptions,
onFilterTapped: (index, selectedFilterId) {
adVM.applyFilterOnMyAds(
adPostStatusEnum:
selectedFilterId.toAdPostEnum());
},
needLeftPadding: false,
).paddingOnly(left: 21),
],
],
);
},
),
16.height,
Expanded(
child: RefreshIndicator(
onRefresh: () async {
if (adVM.isExploreAdsTapped) {
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);
}
},
child: adVM.state == ViewState.busy
? const AdsListShimmer(itemCount: 5)
: AdsListWidget(
isAdsFragment: true,
isDraftAds: false,
shouldShowAdStatus: !adVM.isExploreAdsTapped,
adsList: getAdsList(adVM),
hasMoreData: adVM.isExploreAdsTapped
? adVM.hasMoreDataForExploreAds
: adVM.hasMoreDataForMyAds,
onFetchMoreAds: () async {
AdPostStatus adPostStatusEnum = adVM
.myAdsFilterOptions
.firstWhere(
(element) => element.isSelected)
.id
.toAdPostEnum();
if (adVM.isExploreAdsTapped) {
await adVM.fetchMoreExploreAds();
} else {
await adVM.fetchMoreMyAds(
adsStatus: adPostStatusEnum);
}
adVM.updateIsExploreAds(true);
},
),
),
12.width,
Expanded(
child: ShowFillButton(
isFilled: !adVM.isExploreAdsTapped,
txtColor: !adVM.isExploreAdsTapped ? MyColors.white : MyColors.darkTextColor,
maxHeight: 55,
fontSize: 15,
title: LocaleKeys.myAds.tr(),
onPressed: () {
adVM.updateIsExploreAds(false);
},
).toViewOnly(context, onTap: () {
navigateWithName(context, AppRoutes.loginWithPassword, arguments: false);
}),
),
],
).horPaddingMain(),
if (adVM.isExploreAdsTapped) ...[
if (adVM.adsFiltersCounter == 0) ...[
16.height,
FiltersList(
showImages: true,
filterList: adVM.exploreAdsFilterOptions,
onFilterTapped: (index, selectedFilterId) {
adVM.applyFilterOnExploreAds(vehicleBrandId: selectedFilterId);
},
needLeftPadding: false,
).paddingOnly(left: 21),
]
] else ...[
16.height,
FiltersList(
filterList: adVM.myAdsFilterOptions,
onFilterTapped: (index, selectedFilterId) {
adVM.applyFilterOnMyAds(adPostStatusEnum: selectedFilterId.toAdPostEnum());
},
needLeftPadding: false,
).paddingOnly(left: 21),
],
],
);
},
),
),
if (adVM.isLoadingMore) ...[
const Center(
child: Padding(
padding: EdgeInsets.all(8.0),
child: CircularProgressIndicator(),
))
]
],
),
16.height,
Expanded(
child: RefreshIndicator(
onRefresh: () async {
if (adVM.isExploreAdsTapped) {
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);
}
},
child: adVM.state == ViewState.busy
? const AdsListShimmer(itemCount: 5)
: AdsListWidget(
isAdsFragment: true,
isDraftAds: false,
shouldShowAdStatus: !adVM.isExploreAdsTapped,
adsList: getAdsList(adVM),
hasMoreData: adVM.isExploreAdsTapped ? adVM.hasMoreDataForExploreAds : adVM.hasMoreDataForMyAds,
onFetchMoreAds: () async {
AdPostStatus adPostStatusEnum = adVM.myAdsFilterOptions.firstWhere((element) => element.isSelected).id.toAdPostEnum();
if (adVM.isExploreAdsTapped) {
await adVM.fetchMoreExploreAds();
} else {
await adVM.fetchMoreMyAds(adsStatus: adPostStatusEnum);
}
},
),
),
// Loading overlay when FAB is clicked and API is fetching
if (adVM.isLoadingAdsDuration)
Container(
width: double.infinity,
height: double.infinity,
color: Colors.black.withOpacity(0.3),
child: const Center(
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(
MyColors.darkPrimaryColor),
),
),
),
if (adVM.isLoadingMore) ...[
const Center(
child: Padding(
padding: EdgeInsets.all(8.0),
child: CircularProgressIndicator(),
))
]
],
),
],
),
floatingActionButton: FloatingActionButton(
onPressed: () async {
adVM.getVehicleAdsDuration();
// ✅ 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,
);

@ -312,7 +312,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(
@ -341,7 +341,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),

@ -121,6 +121,17 @@ class _ShippingManagementViewState extends State<ShippingManagementView> {
},
),
12.height,
// ✅ Show numberOfShipping field only when updating from pending to initiated (first update)
if (shippingRequestStatusEnum == ShippingRequestStatusEnum.pending) ...[
TxtField(
value: shippingManagementVM.numberOfShipping,
errorValue: "",
keyboardType: TextInputType.text,
hint: LocaleKeys.numberOfShipping.tr(),
onChanged: (v) => shippingManagementVM.updateNumberOfShipping(v),
),
12.height,
],
TxtField(
maxLines: 4,
value: shippingManagementVM.requestStatusComments,
@ -202,6 +213,17 @@ class _ShippingManagementViewState extends State<ShippingManagementView> {
},
),
12.height,
// ✅ Show numberOfShipping field only when updating from pending to preparingToCollect (first update)
if (selfPickupRequestStatusEnum == SelfPickupRequestStatusEnum.pending) ...[
TxtField(
value: shippingManagementVM.numberOfShipping,
errorValue: "",
keyboardType: TextInputType.text,
hint: LocaleKeys.numberOfShipping.tr(),
onChanged: (v) => shippingManagementVM.updateNumberOfShipping(v),
),
12.height,
],
TxtField(
maxLines: 4,
value: shippingManagementVM.requestStatusComments,
@ -352,6 +374,10 @@ class _ShippingManagementViewState extends State<ShippingManagementView> {
if (shippingRequest.vehicleType != null && shippingRequest.vehicleType!.isNotEmpty) ...[
("${LocaleKeys.itemType.tr()}: " "${shippingRequest.vehicleType}" ?? "").toText(color: MyColors.lightTextColor, fontSize: 12, fontWeight: MyFonts.Medium),
],
// ✅ Display numberOfShipping when not null
if (shippingRequest.numberOfShipping != null && shippingRequest.numberOfShipping!.isNotEmpty) ...[
("${LocaleKeys.numberOfShipping.tr()}: ${shippingRequest.numberOfShipping}").toText(color: MyColors.lightTextColor, fontSize: 12, fontWeight: MyFonts.Medium),
],
if (shippingRequest.comment != null && shippingRequest.comment!.isNotEmpty) ...[
("${LocaleKeys.comment.tr()}: ${shippingRequest.comment ?? ""}").toText(color: MyColors.lightTextColor, fontSize: 12, fontWeight: MyFonts.Medium),
],

@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:mc_common_app/classes/consts.dart';
import 'package:mc_common_app/config/routes.dart';
import 'package:mc_common_app/utils/navigator.dart';
@ -6,6 +7,8 @@ import 'package:mc_common_app/utils/utils.dart';
import 'package:mc_common_app/view_models/ad_view_model.dart';
import 'package:mc_common_app/view_models/chat_view_model.dart';
import 'package:provider/provider.dart';
import 'package:audioplayers/audioplayers.dart';
import 'dart:typed_data';
class SplashPage extends StatefulWidget {
const SplashPage({super.key});
@ -16,14 +19,29 @@ class SplashPage extends StatefulWidget {
class SplashPageState extends State<SplashPage> with WidgetsBindingObserver {
bool showFlashingLights = false;
final AudioPlayer _audioPlayer = AudioPlayer();
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_playEngineSound();
startSplashTimer();
}
Future<void> _playEngineSound() async {
try {
// Load audio from package assets using rootBundle (same pattern as Image.asset)
// MyAssets.engineStartSound = "packages/mc_common_app/assets/tones/engine_start_tone.mp3"
final ByteData data = await rootBundle.load(MyAssets.engineStartSound);
final Uint8List bytes = data.buffer.asUint8List();
await _audioPlayer.play(BytesSource(bytes));
} catch (e) {
// Ignore errors if sound fails to play
debugPrint('Error playing engine sound: $e');
}
}
void startSplashTimer() {
// After 3 seconds, show the flashing lights gif
Future.delayed(const Duration(seconds: 3), () {
@ -45,6 +63,8 @@ class SplashPageState extends State<SplashPage> with WidgetsBindingObserver {
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
_audioPlayer.stop();
_audioPlayer.dispose();
super.dispose();
}
@ -64,8 +84,12 @@ class SplashPageState extends State<SplashPage> with WidgetsBindingObserver {
// });
// }
navigateToNextDeciderPage() {
navigateReplaceWithName(context, AppRoutes.registerSelection);
navigateToNextDeciderPage() async {
// Stop the audio before navigating
await _audioPlayer.stop();
if (mounted) {
navigateReplaceWithName(context, AppRoutes.registerSelection);
}
}
@override

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

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

@ -25,6 +25,62 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.11.0"
audioplayers:
dependency: "direct main"
description:
name: audioplayers
sha256: f16640453cc47487b7de72a2b28d37c7df1ac97999849f4a46d92b1d2b0f093d
url: "https://pub.dev"
source: hosted
version: "6.7.1"
audioplayers_android:
dependency: transitive
description:
name: audioplayers_android
sha256: "60a6728277228413a85755bd3ffd6fab98f6555608923813ce383b190a360605"
url: "https://pub.dev"
source: hosted
version: "5.2.1"
audioplayers_darwin:
dependency: transitive
description:
name: audioplayers_darwin
sha256: c994b3bb3a921e4904ac40e013fbc94488e824fd7c1de6326f549943b0b44a91
url: "https://pub.dev"
source: hosted
version: "6.4.0"
audioplayers_linux:
dependency: transitive
description:
name: audioplayers_linux
sha256: f75bce1ce864170ef5e6a2c6a61cd3339e1a17ce11e99a25bae4474ea491d001
url: "https://pub.dev"
source: hosted
version: "4.2.1"
audioplayers_platform_interface:
dependency: transitive
description:
name: audioplayers_platform_interface
sha256: "0e2f6a919ab56d0fec272e801abc07b26ae7f31980f912f24af4748763e5a656"
url: "https://pub.dev"
source: hosted
version: "7.1.1"
audioplayers_web:
dependency: transitive
description:
name: audioplayers_web
sha256: "24a6f258062bd7da8cb2157e83fccb9816a08dd306cbaaa24f9813d071470545"
url: "https://pub.dev"
source: hosted
version: "5.2.1"
audioplayers_windows:
dependency: transitive
description:
name: audioplayers_windows
sha256: "95f875a96c88c3dbbcb608d4f8288e300b0113d256a81d0b3197fcc18f0dc91a"
url: "https://pub.dev"
source: hosted
version: "4.3.1"
auto_size_text:
dependency: "direct main"
description:
@ -1484,4 +1540,4 @@ packages:
version: "6.5.0"
sdks:
dart: ">=3.10.0-0 <4.0.0"
flutter: ">=3.24.0"
flutter: ">=3.35.0"

@ -43,6 +43,7 @@ dependencies:
change_app_package_name: ^1.3.0
share_plus: any
toastification: ^2.3.0
audioplayers: ^6.0.0
geolocator_android: 4.5.5
@ -78,6 +79,7 @@ flutter:
assets:
- assets/
- assets/langs/
- assets/tones/
- assets/icons/
- assets/icons/payments/
- assets/images/

Loading…
Cancel
Save