Completed the bugs

faiz_development_common
FaizHashmi 1 week ago
parent 6f6533a1ca
commit c02319fefa

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

@ -474,7 +474,7 @@
"cancelReservation": "Cancel Reservation", "cancelReservation": "Cancel Reservation",
"reasonForCancellation": "Reason for Cancellation", "reasonForCancellation": "Reason for Cancellation",
"doWantCancelReservation": "Do you want to cancel the reservation", "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", "completeDeal": "Complete Deal",
"uploadBankReceipt": "Upload Bank Receipt", "uploadBankReceipt": "Upload Bank Receipt",
"notesForAdmin": "Notes For Admin", "notesForAdmin": "Notes For Admin",
@ -720,6 +720,8 @@
"customerLocation": "Customer Location", "customerLocation": "Customer Location",
"deliveryAvailable": "Delivery Available", "deliveryAvailable": "Delivery Available",
"viewed": "Viewed", "viewed": "Viewed",
"read": "Read",
"sent": "Sent",
"itemNoLongerAvailable": "This item is no longer available.", "itemNoLongerAvailable": "This item is no longer available.",
"reactivateAd": "Reactivate Ad", "reactivateAd": "Reactivate Ad",
"dealOutsideApp": "The deal was done outside the app with another customer.", "dealOutsideApp": "The deal was done outside the app with another customer.",
@ -832,6 +834,7 @@
"myDraftAds": "My Draft Ads", "myDraftAds": "My Draft Ads",
"scheduleDeletedSuccessfully": "The schedule has been deleted successfully", "scheduleDeletedSuccessfully": "The schedule has been deleted successfully",
"addValidAddress": "Please add a valid address", "addValidAddress": "Please add a valid address",
"pleaseAddAtLeastOneImage": "Please add at least one image for your request",
"termsPrivacyPolicy": "Terms & Privacy Policy", "termsPrivacyPolicy": "Terms & Privacy Policy",
"pendingApproval": "Pending Approval", "pendingApproval": "Pending Approval",
"inReview": "In Review", "inReview": "In Review",

@ -3,6 +3,7 @@ import 'dart:convert';
import 'dart:developer'; import 'dart:developer';
import 'dart:io'; import 'dart:io';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:http/http.dart'; import 'package:http/http.dart';
@ -70,6 +71,22 @@ abstract class ApiClient {
} }
class ApiClientImp implements 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 @override
Future<U> postJsonForObject<T, U>(FactoryConstructor<U> factoryConstructor, String url, T jsonObject, 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 { {String? token, Map<String, dynamic>? queryParameters, Map<String, String>? headers, int retryTimes = 0}) async {
@ -117,6 +134,10 @@ class ApiClientImp implements ApiClient {
}) async { }) async {
try { try {
var headers0 = <String, String>{}; var headers0 = <String, String>{};
// Add Accept-Language header based on current locale
headers0['Accept-Language'] = _getCurrentLanguage();
if (token != null) { if (token != null) {
headers0['Authorization'] = 'Bearer $token'; 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 { Future<Response> _getForResponse(String url, {String? token, Map<String, dynamic>? queryParameters, Map<String, String>? headers, int retryTimes = 0}) async {
try { try {
var headers0 = <String, String>{}; var headers0 = <String, String>{};
// Add Accept-Language header based on current locale
headers0['Accept-Language'] = _getCurrentLanguage();
if (token != null) { if (token != null) {
headers0['Authorization'] = 'Bearer $token'; headers0['Authorization'] = 'Bearer $token';
} }

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

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

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

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

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

@ -7,6 +7,7 @@ import 'package:mc_common_app/classes/consts.dart';
import 'package:mc_common_app/config/dependency_injection.dart'; import 'package:mc_common_app/config/dependency_injection.dart';
import 'package:mc_common_app/extensions/string_extensions.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/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/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_bank_details_model.dart';
import 'package:mc_common_app/models/advertisment_models/ads_duration_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>> getMyAds();
Future<List<AdDetailsModel>> getFavouriteAds(); Future<List<AdDetailsModel>> getFavouriteAds({
AdPostStatus? adPostStatus,
CreatedByRoleEnum? createdByRoleEnum,
int? vehicleBrandId,
});
Future<GenericRespModel> toggleAdFavorite({required int adsID, required bool isFavorite}); Future<GenericRespModel> toggleAdFavorite({required int adsID, required bool isFavorite});
@ -455,16 +460,17 @@ class AdsRepoImp implements AdsRepo {
); );
if (adsGenericModel.messageStatus != 1 || adsGenericModel.data == null) { if (adsGenericModel.messageStatus != 1 || adsGenericModel.data == null) {
Utils.showToast(adsGenericModel.message ?? LocaleKeys.somethingWrong.tr()); throw Exception(adsGenericModel.message ?? LocaleKeys.somethingWrong.tr());
return [];
} }
List<MyReservedAdsRespModel> reservedAds = List.generate(adsGenericModel.data.length, (index) => MyReservedAdsRespModel.fromJson(adsGenericModel.data[index])); 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) { if (reservedAds.isEmpty) {
return []; 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); return await getAdsPerSpecificIds(ids: selectedIdsString, reservedAds: reservedAds);
} }
@ -472,7 +478,7 @@ class AdsRepoImp implements AdsRepo {
var params = { var params = {
"AdsIDs": ids, "AdsIDs": ids,
"isActive": "true", "isActive": "true",
"PageSize": "30", "PageSize": ids.length.toString(), // Request all reserved ads in one call
}; };
GenericRespModel adsGenericModel = await apiClient.getJsonForObject( GenericRespModel adsGenericModel = await apiClient.getJsonForObject(
token: appState.getUser.data!.accessToken, token: appState.getUser.data!.accessToken,
@ -481,16 +487,49 @@ class AdsRepoImp implements AdsRepo {
ApiConsts.vehicleAdsGet, ApiConsts.vehicleAdsGet,
); );
if (adsGenericModel.messageStatus != 1 || adsGenericModel.data == null) { if (adsGenericModel.messageStatus != 1 || adsGenericModel.data == null) {
Utils.showToast(adsGenericModel.message ?? LocaleKeys.somethingWrong.tr()); throw Exception(adsGenericModel.message ?? LocaleKeys.somethingWrong.tr());
return [];
} }
List<AdDetailsModel> vehicleAdsDetails = List.generate(adsGenericModel.data.length, (index) => AdDetailsModel.fromJson(adsGenericModel.data[index], true, adsGenericModel.totalItemsCount ?? 1)); 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++) { // Create a map for O(1) lookup: adsID -> reserve status
vehicleAdsDetails[i].adReserveStatus = (reservedAds[i].adsReserveStatus ?? 0).toAdRserveStatusEnum(); 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; return vehicleAdsDetails;
} }
@ -515,16 +554,44 @@ class AdsRepoImp implements AdsRepo {
} }
@override @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( GenericRespModel adsGenericModel = await apiClient.getJsonForObject(
token: appState.getUser.data!.accessToken, token: appState.getUser.data!.accessToken,
(json) => GenericRespModel.fromJson(json), (json) => GenericRespModel.fromJson(json),
ApiConsts.favouriteAdsGet, ApiConsts.favouriteAdsGet,
queryParameters: params,
); );
if (adsGenericModel.messageStatus != 1 || adsGenericModel.data == null) { if (adsGenericModel.messageStatus != 1 || adsGenericModel.data == null) {
return []; return [];
} }
List<AdDetailsModel> favouriteAdsDetails = List.generate(adsGenericModel.data.length, (index) => AdDetailsModel.fromJson(adsGenericModel.data[index], false, adsGenericModel.totalItemsCount ?? 1)); List<AdDetailsModel> favouriteAdsDetails = List.generate(adsGenericModel.data.length, (index) => AdDetailsModel.fromJson(adsGenericModel.data[index], false, adsGenericModel.totalItemsCount ?? 1));
return favouriteAdsDetails; return favouriteAdsDetails;
} }
@ -638,7 +705,7 @@ class AdsRepoImp implements AdsRepo {
String token = appState.getUser.data!.accessToken ?? ""; String token = appState.getUser.data!.accessToken ?? "";
GenericRespModel adsGenericModel = await apiClient.postJsonForObject( GenericRespModel adsGenericModel = await apiClient.postJsonForObject(
(json) => GenericRespModel.fromJson(json), (json) => GenericRespModel.fromJson(json),
ApiConsts.deleteAd, ApiConsts.adsReserveCancelByOwner,
postParams, postParams,
token: token, token: token,
); );

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

@ -1,13 +1,18 @@
import 'dart:convert';
import 'dart:developer';
import 'dart:io'; import 'dart:io';
import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:mc_common_app/classes/app_state.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/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'; import 'package:permission_handler/permission_handler.dart';
abstract class FirebaseMessagingService { abstract class FirebaseMessagingService {
@ -63,17 +68,39 @@ class FirebaseMessagingServiceImp implements FirebaseMessagingService {
defaultPresentSound: true, defaultPresentSound: true,
); );
final InitializationSettings initializationSettings = InitializationSettings(android: initializationSettingsAndroid, iOS: initializationSettingsDarwin); 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(); RemoteMessage? initialMessage = await FirebaseMessaging.instance.getInitialMessage();
if (initialMessage != null) _handleNotification(initialMessage); if (initialMessage != null) _handleNotification(initialMessage);
FirebaseMessaging.onMessage.listen((RemoteMessage message) { 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); FirebaseMessaging.onBackgroundMessage(backgroundMessageHandler);
@ -86,14 +113,208 @@ class FirebaseMessagingServiceImp implements FirebaseMessagingService {
logger.i(body); 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 { void _handleNotification(RemoteMessage message) async {
logger.i(message.notification!.title); // This is called when app is terminated and opened by notification
logger.i(message.notification!.body); 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( const AndroidNotificationDetails androidNotificationDetails = AndroidNotificationDetails(
"0", "0",
'mowater', 'mowater',
@ -105,7 +326,7 @@ class FirebaseMessagingServiceImp implements FirebaseMessagingService {
const DarwinNotificationDetails iOSPlatformChannelSpecifics = DarwinNotificationDetails(threadIdentifier: 'thread_id'); const DarwinNotificationDetails iOSPlatformChannelSpecifics = DarwinNotificationDetails(threadIdentifier: 'thread_id');
const NotificationDetails notificationDetails = NotificationDetails(android: androidNotificationDetails, iOS: iOSPlatformChannelSpecifics); 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 { enum DashboardRouteEnum {
fromAdsPayment, fromAdsPayment,
fromAdsSubmit, fromAdsSubmit,
fromReserveAdsPayment,
none, 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) { static Color getChipColorByRequestStatus(RequestStatusEnum requestStatus) {
switch (requestStatus) { switch (requestStatus) {
case RequestStatusEnum.pending: case RequestStatusEnum.pending:

@ -295,6 +295,16 @@ class AdVM extends BaseVM {
return await adsRepo.getAllAds(isMyAds: isMyAds, adPostStatus: adPostStatus, createdByRoleEnum: createdByRoleEnum, vehicleBrandId: vehicleBrandId); 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 { applyFilterOnMyAds({required AdPostStatus adPostStatusEnum}) async {
pageIndexForMyAds = 1; pageIndexForMyAds = 1;
hasMoreDataForMyAds = true; hasMoreDataForMyAds = true;
@ -310,9 +320,15 @@ class AdVM extends BaseVM {
getMyAds(); getMyAds();
return; return;
} }
// this means if the filter is reserved ads // this means if the filter is reserved ads
if (adPostStatusEnum == AdPostStatus.reserved) { 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; myAdsFilteredList = myReservedAds;
for (var ad in myAdsFilteredList) { for (var ad in myAdsFilteredList) {
ad.isReservedByMe = true; ad.isReservedByMe = true;
@ -320,6 +336,7 @@ class AdVM extends BaseVM {
notifyListeners(); notifyListeners();
return; return;
} }
setState(ViewState.busy); setState(ViewState.busy);
myAdsFilteredList = await getAdsByFilter(adPostStatus: adPostStatusEnum, isMyAds: true); myAdsFilteredList = await getAdsByFilter(adPostStatus: adPostStatusEnum, isMyAds: true);
setState(ViewState.idle); setState(ViewState.idle);
@ -385,10 +402,18 @@ class AdVM extends BaseVM {
} }
} }
Future<void> getFavouriteAds() async { Future<void> getFavouriteAds({
AdPostStatus? adPostStatus,
CreatedByRoleEnum? createdByRoleEnum,
int? vehicleBrandId,
}) async {
try { try {
setState(ViewState.busy); setState(ViewState.busy);
favouriteAds = await adsRepo.getFavouriteAds(); favouriteAds = await adsRepo.getFavouriteAds(
adPostStatus: adPostStatus,
createdByRoleEnum: createdByRoleEnum,
vehicleBrandId: vehicleBrandId,
);
notifyListeners(); notifyListeners();
setState(ViewState.idle); setState(ViewState.idle);
} catch (e) { } catch (e) {
@ -433,7 +458,8 @@ class AdVM extends BaseVM {
myAdsFilteredList = myAds; myAdsFilteredList = myAds;
final myActiveAds = myAds.where((element) => element.adPostStatus == AdPostStatus.active).toList(); final myActiveAds = myAds.where((element) => element.adPostStatus == AdPostStatus.active).toList();
myActiveAdsForHome = myActiveAds.length >= 3 ? myActiveAds.take(3).toList() : myActiveAds; 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(); notifyListeners();
setState(ViewState.idle); setState(ViewState.idle);
} }
@ -442,12 +468,19 @@ class AdVM extends BaseVM {
try { try {
setState(ViewState.busy); setState(ViewState.busy);
myReservedAds = await adsRepo.getMyReservedAds(); myReservedAds = await adsRepo.getMyReservedAds();
isFetchingLists = false;
setState(ViewState.idle); setState(ViewState.idle);
notifyListeners(); // Notify UI of data changes
return myReservedAds; return myReservedAds;
} catch (e) { } catch (e) {
logger.e(e.toString()); logger.e("Error fetching reserved ads: ${e.toString()}");
setState(ViewState.idle); 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 []; return [];
} }
} }
@ -538,6 +571,11 @@ class AdVM extends BaseVM {
Utils.showToast(respModel.message ?? LocaleKeys.somethingWrong.tr()); Utils.showToast(respModel.message ?? LocaleKeys.somethingWrong.tr());
return; return;
} }
// Reset the cancel reason text field
reservationCancelReason = "";
reservationCancelError = "";
Utils.hideLoading(context); Utils.hideLoading(context);
Utils.showToast(LocaleKeys.yourReservationCancelled.tr()); Utils.showToast(LocaleKeys.yourReservationCancelled.tr());
updateIsExploreAds(false); updateIsExploreAds(false);
@ -2733,15 +2771,23 @@ class AdVM extends BaseVM {
bool isArabic = AppState().isArabic(context); bool isArabic = AppState().isArabic(context);
updateSelectionVehicleModelId(SelectionModel(selectedId: previousAdDetails!.vehicle!.model!.id!, selectedOption: previousAdDetails!.vehicle!.model!.label ?? "")); updateSelectionVehicleModelId(SelectionModel(selectedId: previousAdDetails!.vehicle!.model!.id!, selectedOption: previousAdDetails!.vehicle!.model!.label ?? ""));
updateSelectionVehicleModelYearId(SelectionModel(selectedId: previousAdDetails!.vehicle!.modelyear!.id!, selectedOption: previousAdDetails!.vehicle!.modelyear!.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 ?? "")); updateSelectionVehicleColorId(SelectionModel(
updateSelectionVehicleConditionId(SelectionModel(selectedId: previousAdDetails!.vehicle!.condition!.id!, selectedOption: isArabic ? previousAdDetails!.vehicle!.condition!.labelN.toString() :previousAdDetails!.vehicle!.condition!.label ?? "")); selectedId: previousAdDetails!.vehicle!.color!.id!, selectedOption: isArabic ? previousAdDetails!.vehicle!.color!.labelN.toString() : previousAdDetails!.vehicle!.color!.label ?? ""));
updateSelectionVehicleCategoryId(SelectionModel(selectedId: previousAdDetails!.vehicle!.category!.id!, selectedOption: isArabic ? previousAdDetails!.vehicle!.category!.labelN.toString() :previousAdDetails!.vehicle!.category!.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( updateSelectionVehicleMileageId(SelectionModel(
selectedId: previousAdDetails!.vehicle!.mileage!.id!, selectedId: previousAdDetails!.vehicle!.mileage!.id!,
selectedOption: "${previousAdDetails!.vehicle!.mileage!.mileageStart} - ${previousAdDetails!.vehicle!.mileage!.mileageEnd}", 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 ?? "")); updateSelectionVehicleTransmissionId(SelectionModel(
updateSelectionVehicleSellerTypeId(SelectionModel(selectedId: previousAdDetails!.vehicle!.sellertype!.id!, selectedOption: isArabic ? previousAdDetails!.vehicle!.sellertype!.labelN.toString() :previousAdDetails!.vehicle!.sellertype!.label ?? "")); 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); int indexCountry = vehicleCountries.indexWhere((element) => element.id == previousAdDetails!.vehicle!.countryID);
if (indexCountry != -1) { if (indexCountry != -1) {
updateSelectionVehicleCountryId(SelectionModel(selectedId: vehicleCountries[indexCountry].id!, selectedOption: vehicleCountries[indexCountry].countryName ?? "")); updateSelectionVehicleCountryId(SelectionModel(selectedId: vehicleCountries[indexCountry].id!, selectedOption: vehicleCountries[indexCountry].countryName ?? ""));

@ -225,6 +225,28 @@ class ChatVM extends BaseVM {
int providerIndex = serviceProviderOffersList.indexWhere((element) => element.providerUserId == currentMessage.senderUserID); int providerIndex = serviceProviderOffersList.indexWhere((element) => element.providerUserId == currentMessage.senderUserID);
if (providerIndex != -1) { 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) { if (currentMessage.chatMessageTypeEnum == ChatMessageTypeEnum.offer) {
for (var chatMessage in serviceProviderOffersList[providerIndex].chatMessages!) { for (var chatMessage in serviceProviderOffersList[providerIndex].chatMessages!) {
if (chatMessage.chatMessageTypeEnum == ChatMessageTypeEnum.offer && chatMessage.reqOffer!.id == currentMessage.reqOffer!.id) { if (chatMessage.chatMessageTypeEnum == ChatMessageTypeEnum.offer && chatMessage.reqOffer!.id == currentMessage.reqOffer!.id) {
@ -248,6 +270,28 @@ class ChatVM extends BaseVM {
} }
log("requestIndex: $requestIndex"); log("requestIndex: $requestIndex");
if (requestIndex != -1) { 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) { if (currentMessage.chatMessageTypeEnum == ChatMessageTypeEnum.offer) {
for (var chatMessage in requestsVM.myFilteredRequests[requestIndex].chatMessages) { for (var chatMessage in requestsVM.myFilteredRequests[requestIndex].chatMessages) {
if (chatMessage.chatMessageTypeEnum == ChatMessageTypeEnum.offer && chatMessage.reqOffer!.id == currentMessage.reqOffer!.id) { if (chatMessage.chatMessageTypeEnum == ChatMessageTypeEnum.offer && chatMessage.reqOffer!.id == currentMessage.reqOffer!.id) {
@ -256,6 +300,13 @@ class ChatVM extends BaseVM {
chatMessage.reqOffer!.requestOfferStatusEnum = currentMessage.reqOffer!.requestOfferStatusEnum; 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); requestsVM.addChatMessagesInRequestsModel(msg: currentMessage, index: requestIndex);
scrollChatDown(); scrollChatDown();
@ -271,6 +322,13 @@ class ChatVM extends BaseVM {
return; 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) { hubConnection!.on(SignalrConsts.receiveMessageRequestOffer, (List<Object?>? arguments) {
logger.i(arguments); logger.i(arguments);
@ -328,6 +386,7 @@ class ChatVM extends BaseVM {
hubConnection!.off(SignalrConsts.receiveMessageRequestOffer); hubConnection!.off(SignalrConsts.receiveMessageRequestOffer);
hubConnection!.off(SignalrConsts.receiveMessageAds); hubConnection!.off(SignalrConsts.receiveMessageAds);
hubConnection!.off(SignalrConsts.receiveMessageGeneral); hubConnection!.off(SignalrConsts.receiveMessageGeneral);
hubConnection!.off(SignalrConsts.requestOfferReadUpdate);
_isSubscribed = false; _isSubscribed = false;
logger.i("Cleared all SignalR listeners"); logger.i("Cleared all SignalR listeners");
} catch (e) { } catch (e) {
@ -371,8 +430,10 @@ class ChatVM extends BaseVM {
_subscribeToReceiveRequestOfferMessages(); _subscribeToReceiveRequestOfferMessages();
_subscribeToReceiveAdMessages(); _subscribeToReceiveAdMessages();
_subscribeToReceiveGeneralMessages(); _subscribeToReceiveGeneralMessages();
_subscribeToRequestOfferReadUpdate();
_isSubscribed = true; _isSubscribed = true;
logger.i("All listeners subscribed successfully"); log("✅ All SignalR listeners subscribed | Hub: ${hubConnection!.connectionId}");
log("📍 Events: ReceiveMessageRequestOffer, ReceiveMessageAds, ReceiveMessageGeneral, RequestOfferReadUpdate");
} else { } else {
logger.w("ViewModels not injected yet. Listeners will be subscribed after injection."); logger.w("ViewModels not injected yet. Listeners will be subscribed after injection.");
} }
@ -392,13 +453,14 @@ class ChatVM extends BaseVM {
hubConnection!.onreconnected((connectionId) async { hubConnection!.onreconnected((connectionId) async {
logger.i("onReconnected: ${connectionId.toString()}"); logger.i("onReconnected: ${connectionId.toString()}");
_retryCount = 0; // Reset on successful reconnect _retryCount = 0; // Reset on successful reconnect
// CRITICAL FIX: Re-subscribe listeners after automatic reconnect // CRITICAL FIX: Re-subscribe listeners after automatic reconnect
if (_requestsVM != null && _adVM != null) { if (_requestsVM != null && _adVM != null) {
_clearAllListeners(); _clearAllListeners();
_subscribeToReceiveRequestOfferMessages(); _subscribeToReceiveRequestOfferMessages();
_subscribeToReceiveAdMessages(); _subscribeToReceiveAdMessages();
_subscribeToReceiveGeneralMessages(); _subscribeToReceiveGeneralMessages();
_subscribeToRequestOfferReadUpdate();
_isSubscribed = true; _isSubscribed = true;
logger.i("✅ Listeners re-subscribed after automatic reconnection"); logger.i("✅ Listeners re-subscribed after automatic reconnection");
} else { } else {
@ -643,7 +705,7 @@ class ChatVM extends BaseVM {
} }
} }
} }
isSendingMessage = false; isSendingMessage = false;
notifyListeners(); notifyListeners();
return true; return true;
@ -655,7 +717,7 @@ class ChatVM extends BaseVM {
return false; return false;
} }
} }
isSendingMessage = false; isSendingMessage = false;
notifyListeners(); notifyListeners();
return false; return false;
@ -873,6 +935,13 @@ class ChatVM extends BaseVM {
return; 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) { hubConnection!.on(SignalrConsts.receiveMessageAds, (List<Object?>? arguments) {
if (arguments == null || arguments.isEmpty) return; if (arguments == null || arguments.isEmpty) return;
List<ChatMessageModel> chat = []; List<ChatMessageModel> chat = [];
@ -889,6 +958,23 @@ class ChatVM extends BaseVM {
Future<void> onNewMessageReceivedForAds({required List<ChatMessageModel> messages}) async { Future<void> onNewMessageReceivedForAds({required List<ChatMessageModel> messages}) async {
for (var msg in messages) { 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); currentMessagesForAds.add(msg);
} }
scrollChatDown(); scrollChatDown();
@ -965,7 +1051,7 @@ class ChatVM extends BaseVM {
if (AppState().currentAppType == AppType.customer) { if (AppState().currentAppType == AppType.customer) {
} else {} } else {}
isSendingMessage = false; isSendingMessage = false;
notifyListeners(); notifyListeners();
return true; return true;
@ -977,7 +1063,7 @@ class ChatVM extends BaseVM {
return false; return false;
} }
} }
isSendingMessage = false; isSendingMessage = false;
notifyListeners(); notifyListeners();
return false; return false;
@ -1021,6 +1107,23 @@ class ChatVM extends BaseVM {
Future<void> onNewMessageReceivedForGeneral({required List<ChatMessageModel> messages}) async { Future<void> onNewMessageReceivedForGeneral({required List<ChatMessageModel> messages}) async {
for (var msg in messages) { 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 currentMessagesForGeneralChat.add(msg); // FIXED: Use correct list
} }
scrollChatDown(); scrollChatDown();
@ -1028,6 +1131,13 @@ class ChatVM extends BaseVM {
} }
void _subscribeToReceiveGeneralMessages() { 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 // General messages don't need specific VM, just storing messages
hubConnection!.on(SignalrConsts.receiveMessageGeneral, (List<Object?>? arguments) { hubConnection!.on(SignalrConsts.receiveMessageGeneral, (List<Object?>? arguments) {
if (arguments == null || arguments.isEmpty) return; if (arguments == null || arguments.isEmpty) return;
@ -1043,6 +1153,120 @@ class ChatVM extends BaseVM {
logger.i("Subscribed to General messages"); 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({ Future<bool> onTextMessageSendForGeneralChat({
required String receiverId, required String receiverId,
required ChatMessageTypeEnum chatMessageType, required ChatMessageTypeEnum chatMessageType,
@ -1110,7 +1334,7 @@ class ChatVM extends BaseVM {
if (AppState().currentAppType == AppType.customer) { if (AppState().currentAppType == AppType.customer) {
} else {} } else {}
isSendingMessage = false; isSendingMessage = false;
notifyListeners(); notifyListeners();
return true; return true;
@ -1122,7 +1346,7 @@ class ChatVM extends BaseVM {
return false; return false;
} }
} }
isSendingMessage = false; isSendingMessage = false;
notifyListeners(); notifyListeners();
return false; return false;

@ -42,26 +42,26 @@ class DashboardVmCustomer extends BaseVM with WidgetsBindingObserver {
@override @override
void didChangeAppLifecycleState(AppLifecycleState state) { void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state); super.didChangeAppLifecycleState(state);
logger.i("App lifecycle state changed to: $state"); log("App lifecycle state changed to: $state");
if (_context == null) return; if (_context == null) return;
final chatVM = Provider.of<ChatVM>(_context!, listen: false); final chatVM = Provider.of<ChatVM>(_context!, listen: false);
switch (state) { switch (state) {
case AppLifecycleState.resumed: 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"); logger.i("App resumed - checking SignalR connection");
chatVM.buildHubConnection(context: _context); chatVM.buildHubConnection(context: _context);
break; break;
case AppLifecycleState.paused: 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"); logger.i("App paused - SignalR staying connected");
break; break;
case AppLifecycleState.inactive: case AppLifecycleState.inactive:
case AppLifecycleState.detached: case AppLifecycleState.detached:
case AppLifecycleState.hidden: case AppLifecycleState.hidden:
// Other states - no action needed // Other states - no action needed
break; break;
} }
} }

@ -1,6 +1,7 @@
import 'dart:developer'; import 'dart:developer';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/scheduler.dart';
import 'package:mc_common_app/classes/app_state.dart'; import 'package:mc_common_app/classes/app_state.dart';
import 'package:mc_common_app/config/routes.dart'; import 'package:mc_common_app/config/routes.dart';
import 'package:mc_common_app/extensions/string_extensions.dart'; import 'package:mc_common_app/extensions/string_extensions.dart';
@ -85,8 +86,8 @@ class PaymentVM extends ChangeNotifier {
); );
// Utils.hideLoading(context); // Commented out: matching pair with showLoading // Utils.hideLoading(context); // Commented out: matching pair with showLoading
log("isPaid: ${isPaid}"); log("isPaid: $isPaid");
// TODO: VERIFY THIS WITH ZAHOOR
if (!isPaid) { if (!isPaid) {
// Utils.showToast(LocaleKeys.paymentFailed.tr()); // Utils.showToast(LocaleKeys.paymentFailed.tr());
// pop(context); // pop(context);
@ -94,9 +95,11 @@ class PaymentVM extends ChangeNotifier {
} else { } else {
Utils.showToast(LocaleKeys.paymentSuccessful.tr()); Utils.showToast(LocaleKeys.paymentSuccessful.tr());
} }
// Call onVerified callback - success methods handle their own context checks
onVerified(); onVerified();
} catch (e) { } catch (e) {
log("verifyPayments: ${e.toString()}"); log("verifyPayments error: ${e.toString()}");
Utils.showToast(e.toString()); Utils.showToast(e.toString());
return; return;
} }
@ -147,7 +150,7 @@ class PaymentVM extends ChangeNotifier {
context: context, context: context,
paymentTypeId: paymentTypeEnum.getIdFromPaymentTypesEnum(), paymentTypeId: paymentTypeEnum.getIdFromPaymentTypesEnum(),
id: orderProviderSubscriptionId, id: orderProviderSubscriptionId,
onVerified: () => onSubscriptionPaymentSuccess(context), onVerified: () => onSubscriptionPaymentSuccess(),
); );
break; break;
@ -173,6 +176,14 @@ class PaymentVM extends ChangeNotifier {
break; break;
case PaymentTypes.adReserve: case PaymentTypes.adReserve:
await verifyPayments(
context: context,
paymentTypeId: paymentTypeEnum.getIdFromPaymentTypesEnum(),
id: currentAdId,
onVerified: () => onAdReservePaymentSuccess(context),
);
break;
case PaymentTypes.ads: case PaymentTypes.ads:
case PaymentTypes.extendAds: case PaymentTypes.extendAds:
await verifyPayments( await verifyPayments(
@ -187,6 +198,18 @@ class PaymentVM extends ChangeNotifier {
} }
void onRequestPaymentSuccess(BuildContext context) { 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) { if (AppState().currentAppType == AppType.provider) {
context.read<DashboardVMProvider>().onNavbarTapped(4); context.read<DashboardVMProvider>().onNavbarTapped(4);
} else { } else {
@ -196,6 +219,19 @@ class PaymentVM extends ChangeNotifier {
} }
void onAdsPaymentSuccess(BuildContext context) { 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); context.read<AdVM>().updateIsExploreAds(false);
if (AppState().currentAppType == AppType.provider) { if (AppState().currentAppType == AppType.provider) {
context.read<DashboardVMProvider>().onNavbarTapped(3); context.read<DashboardVMProvider>().onNavbarTapped(3);
@ -207,14 +243,68 @@ class PaymentVM extends ChangeNotifier {
navigateReplaceWithNameUntilRoute(context, AppRoutes.dashboard, arguments: DashboardRouteEnum.fromAdsPayment); navigateReplaceWithNameUntilRoute(context, AppRoutes.dashboard, arguments: DashboardRouteEnum.fromAdsPayment);
} }
void onSubscriptionPaymentSuccess(BuildContext context) { void onAdReservePaymentSuccess(BuildContext context) {
log("onAdReservePaymentSuccess - Reserve ads payment completed");
// Always use navigatorKey for reliable navigation after payment
// Payment flow often unmounts context, so navigatorKey is more reliable
log("Navigating to dashboard with reserved ads filter");
// Navigate to dashboard first
navigatorKey.currentState?.pushNamedAndRemoveUntil(
AppRoutes.dashboard,
(route) => false,
arguments: DashboardRouteEnum.fromReserveAdsPayment,
);
// Use a post-frame callback to ensure navigation completes before updating state
WidgetsBinding.instance.addPostFrameCallback((_) {
final context = navigatorKey.currentContext;
if (context != null && context.mounted) {
try {
// Navigate to My Ads tab and apply Reserved filter
context.read<AdVM>().updateIsExploreAds(false);
if (AppState().currentAppType == AppType.provider) {
context.read<DashboardVMProvider>().onNavbarTapped(3); // My Ads tab
context.read<AdVM>().applyFilterOnMyAds(adPostStatusEnum: AdPostStatus.reserved);
} else {
context.read<DashboardVmCustomer>().onNavbarTapped(3); // My Ads tab
context.read<AdVM>().applyFilterOnMyAds(adPostStatusEnum: AdPostStatus.reserved);
}
log("✅ Successfully navigated to Reserved Ads");
} catch (e) {
log("⚠️ Error updating AdVM after navigation: $e");
}
} else {
log("⚠️ Context not available for post-navigation updates");
}
});
}
void onSubscriptionPaymentSuccess() {
log("onSubscriptionPaymentSuccess"); log("onSubscriptionPaymentSuccess");
pop(context); // Use navigatorKey to navigate without context dependency
// This works even after payment browser is closed and context is unmounted
navigatorKey.currentState?.pushNamedAndRemoveUntil(
AppRoutes.dashboard,
(route) => false,
);
// context.read<SubscriptionsVM>().getSubscriptionBySP(AppState().getUser.data?.userInfo?.providerId.toString() ?? "", true); // context.read<SubscriptionsVM>().getSubscriptionBySP(AppState().getUser.data?.userInfo?.providerId.toString() ?? "", true);
navigateReplaceWithNameUntilRoute(context, AppRoutes.dashboard);
} }
void onAppointmentSuccess(BuildContext context) { 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) { if (AppState().currentAppType == AppType.provider) {
context.read<DashboardVMProvider>().onNavbarTapped(1); context.read<DashboardVMProvider>().onNavbarTapped(1);
} else { } else {

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

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

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

@ -275,7 +275,18 @@ class _AdCardState extends State<AdCard> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
children: [ children: [
if (isAdsFragment && widget.adDetails.adPostStatus! == AdPostStatus.sold && widget.adDetails.statuslabel != null && widget.adDetails.statuslabel!.isNotEmpty) ...[ // When viewing reserved ads filter, show ONLY reserve status badge
if (widget.adDetails.isReservedByMe == true &&
widget.adDetails.adReserveStatus != null &&
widget.adDetails.adReserveStatus != AdReserveStatus.defaultStatus &&
context.read<AdVM>().isReservedFilterSelected) ...[
Utils.statusContainerChip(
text: Utils.getReserveStatusLabel(widget.adDetails.adReserveStatus!),
chipColor: Utils.getChipColorByReserveStatus(widget.adDetails.adReserveStatus!)
),
]
// Otherwise, show regular ad status for MyAds
else if (isAdsFragment && widget.adDetails.adPostStatus! == AdPostStatus.sold && widget.adDetails.statuslabel != null && widget.adDetails.statuslabel!.isNotEmpty) ...[
Utils.statusContainerChip(text: widget.adDetails.statuslabel ?? "", chipColor: Utils.getChipColorByAdStatus(widget.adDetails.adPostStatus!)), 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) ...[ ] 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!)), Utils.statusContainerChip(text: widget.adDetails.statuslabel ?? "", chipColor: Utils.getChipColorByAdStatus(widget.adDetails.adPostStatus!)),

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

@ -780,19 +780,36 @@ class _ChatMessageCustomWidgetState extends State<ChatMessageCustomWidget> {
Widget buildFreeTextDetailsInMessage({required ChatMessageTypeEnum chatMessageTypeEnum}) { Widget buildFreeTextDetailsInMessage({required ChatMessageTypeEnum chatMessageTypeEnum}) {
return Row( return Row(
children: [ children: [
if (chatMessageTypeEnum == ChatMessageTypeEnum.offer && (widget.chatMessageModel.isMyMessage == true) && (widget.chatMessageModel.isRead ?? false)) ...[ // For Provider: Show status and edit button
Row( if (AppState().currentAppType == AppType.provider &&
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 &&
chatMessageTypeEnum == ChatMessageTypeEnum.offer && chatMessageTypeEnum == ChatMessageTypeEnum.offer &&
widget.chatMessageModel.reqOffer!.requestOfferStatusEnum == RequestOfferStatusEnum.offer && widget.chatMessageModel.isMyMessage == true &&
(widget.chatMessageModel.isMyMessage == true && widget.chatMessageModel.reqOffer != null && widget.chatMessageModel.reqOffer!.id != null)) ...[ widget.chatMessageModel.reqOffer != null &&
MyAssets.icEdit.buildSvg(color: MyColors.white, height: 15).onPress(() => onOfferEditIconPressed()), 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( Expanded(
child: Directionality( child: Directionality(

@ -46,21 +46,23 @@ class AdsFragment extends StatelessWidget {
isRemoveBackButton: true, isRemoveBackButton: true,
actions: [ actions: [
Padding( 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( child: Badge(
isLabelVisible: adVM.adsFiltersCounter > 0, isLabelVisible: adVM.adsFiltersCounter > 0,
largeSize: 15, largeSize: 15,
smallSize: 20, smallSize: 20,
backgroundColor: MyColors.darkPrimaryColor, backgroundColor: MyColors.darkPrimaryColor,
label: Text('${adVM.adsFiltersCounter}'), label: Text('${adVM.adsFiltersCounter}'),
child: MyAssets.searchIcon.buildSvg(), child: MyAssets.searchIcon.buildSvg(height: 22, width: 22),
), ),
).onPress(() { ).onPress(() {
navigateWithName(context, AppRoutes.adsFilterView); navigateWithName(context, AppRoutes.adsFilterView);
}) })
], ],
).toViewOnly(context, onTap: () { ).toViewOnly(context, onTap: () {
navigateWithName(context, AppRoutes.loginWithPassword, arguments: false); navigateWithName(context, AppRoutes.loginWithPassword,
arguments: false);
}), }),
body: Stack( body: Stack(
children: [ children: [
@ -71,7 +73,8 @@ class AdsFragment extends StatelessWidget {
children: [ children: [
16.height, 16.height,
Consumer( Consumer(
builder: (BuildContext context, AdVM adVM, Widget? child) { builder:
(BuildContext context, AdVM adVM, Widget? child) {
return Column( return Column(
children: [ children: [
Row( Row(
@ -82,7 +85,9 @@ class AdsFragment extends StatelessWidget {
maxHeight: 55, maxHeight: 55,
fontSize: 15, fontSize: 15,
title: LocaleKeys.exploreAds.tr(), title: LocaleKeys.exploreAds.tr(),
txtColor: adVM.isExploreAdsTapped ? MyColors.white : MyColors.darkTextColor, txtColor: adVM.isExploreAdsTapped
? MyColors.white
: MyColors.darkTextColor,
onPressed: () { onPressed: () {
log("accessToken: ${AppState().getUser.data!.accessToken}"); log("accessToken: ${AppState().getUser.data!.accessToken}");
log("AppState().getUser.data!.userInfo!.userId;: ${AppState().getUser.data!.userInfo!.userId}"); log("AppState().getUser.data!.userInfo!.userId;: ${AppState().getUser.data!.userInfo!.userId}");
@ -98,7 +103,9 @@ class AdsFragment extends StatelessWidget {
Expanded( Expanded(
child: ShowFillButton( child: ShowFillButton(
isFilled: !adVM.isExploreAdsTapped, isFilled: !adVM.isExploreAdsTapped,
txtColor: !adVM.isExploreAdsTapped ? MyColors.white : MyColors.darkTextColor, txtColor: !adVM.isExploreAdsTapped
? MyColors.white
: MyColors.darkTextColor,
maxHeight: 55, maxHeight: 55,
fontSize: 15, fontSize: 15,
title: LocaleKeys.myAds.tr(), title: LocaleKeys.myAds.tr(),
@ -106,7 +113,9 @@ class AdsFragment extends StatelessWidget {
adVM.updateIsExploreAds(false); adVM.updateIsExploreAds(false);
}, },
).toViewOnly(context, onTap: () { ).toViewOnly(context, onTap: () {
navigateWithName(context, AppRoutes.loginWithPassword, arguments: false); navigateWithName(
context, AppRoutes.loginWithPassword,
arguments: false);
}), }),
), ),
], ],
@ -118,7 +127,8 @@ class AdsFragment extends StatelessWidget {
showImages: true, showImages: true,
filterList: adVM.exploreAdsFilterOptions, filterList: adVM.exploreAdsFilterOptions,
onFilterTapped: (index, selectedFilterId) { onFilterTapped: (index, selectedFilterId) {
adVM.applyFilterOnExploreAds(vehicleBrandId: selectedFilterId); adVM.applyFilterOnExploreAds(
vehicleBrandId: selectedFilterId);
}, },
needLeftPadding: false, needLeftPadding: false,
).paddingOnly(left: 21), ).paddingOnly(left: 21),
@ -128,7 +138,9 @@ class AdsFragment extends StatelessWidget {
FiltersList( FiltersList(
filterList: adVM.myAdsFilterOptions, filterList: adVM.myAdsFilterOptions,
onFilterTapped: (index, selectedFilterId) { onFilterTapped: (index, selectedFilterId) {
adVM.applyFilterOnMyAds(adPostStatusEnum: selectedFilterId.toAdPostEnum()); adVM.applyFilterOnMyAds(
adPostStatusEnum:
selectedFilterId.toAdPostEnum());
}, },
needLeftPadding: false, needLeftPadding: false,
).paddingOnly(left: 21), ).paddingOnly(left: 21),
@ -142,11 +154,19 @@ class AdsFragment extends StatelessWidget {
child: RefreshIndicator( child: RefreshIndicator(
onRefresh: () async { onRefresh: () async {
if (adVM.isExploreAdsTapped) { if (adVM.isExploreAdsTapped) {
int vehicleBrandId = adVM.exploreAdsFilterOptions.firstWhere((element) => element.isSelected).id; int vehicleBrandId = adVM.exploreAdsFilterOptions
adVM.applyFilterOnExploreAds(vehicleBrandId: vehicleBrandId); .firstWhere((element) => element.isSelected)
.id;
adVM.applyFilterOnExploreAds(
vehicleBrandId: vehicleBrandId);
} else { } else {
AdPostStatus adPostStatusEnum = adVM.myAdsFilterOptions.firstWhere((element) => element.isSelected).id.toAdPostEnum(); AdPostStatus adPostStatusEnum = adVM
adVM.applyFilterOnMyAds(adPostStatusEnum: adPostStatusEnum); .myAdsFilterOptions
.firstWhere((element) => element.isSelected)
.id
.toAdPostEnum();
adVM.applyFilterOnMyAds(
adPostStatusEnum: adPostStatusEnum);
} }
}, },
child: adVM.state == ViewState.busy child: adVM.state == ViewState.busy
@ -156,14 +176,22 @@ class AdsFragment extends StatelessWidget {
isDraftAds: false, isDraftAds: false,
shouldShowAdStatus: !adVM.isExploreAdsTapped, shouldShowAdStatus: !adVM.isExploreAdsTapped,
adsList: getAdsList(adVM), adsList: getAdsList(adVM),
hasMoreData: adVM.isExploreAdsTapped ? adVM.hasMoreDataForExploreAds : adVM.hasMoreDataForMyAds, hasMoreData: adVM.isExploreAdsTapped
? adVM.hasMoreDataForExploreAds
: adVM.hasMoreDataForMyAds,
onFetchMoreAds: () async { onFetchMoreAds: () async {
AdPostStatus adPostStatusEnum = adVM.myAdsFilterOptions.firstWhere((element) => element.isSelected).id.toAdPostEnum(); AdPostStatus adPostStatusEnum = adVM
.myAdsFilterOptions
.firstWhere(
(element) => element.isSelected)
.id
.toAdPostEnum();
if (adVM.isExploreAdsTapped) { if (adVM.isExploreAdsTapped) {
await adVM.fetchMoreExploreAds(); await adVM.fetchMoreExploreAds();
} else { } else {
await adVM.fetchMoreMyAds(adsStatus: adPostStatusEnum); await adVM.fetchMoreMyAds(
adsStatus: adPostStatusEnum);
} }
}, },
), ),
@ -187,7 +215,8 @@ class AdsFragment extends StatelessWidget {
color: Colors.black.withOpacity(0.3), color: Colors.black.withOpacity(0.3),
child: const Center( child: const Center(
child: CircularProgressIndicator( child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(MyColors.darkPrimaryColor), valueColor: AlwaysStoppedAnimation<Color>(
MyColors.darkPrimaryColor),
), ),
), ),
), ),
@ -197,30 +226,38 @@ class AdsFragment extends StatelessWidget {
onPressed: () async { onPressed: () async {
// Await API calls with loading state // Await API calls with loading state
await adVM.getVehicleAdsDuration(); await adVM.getVehicleAdsDuration();
// Ensure vehicle types are loaded // Ensure vehicle types are loaded
if (adVM.vehicleTypes.isEmpty) { if (adVM.vehicleTypes.isEmpty) {
await adVM.getVehicleTypes(); await adVM.getVehicleTypes();
} }
if (AppState().userType != UserType.providerDealer) { if (AppState().userType != UserType.providerDealer) {
if (adVM.vehicleAdDurationId.selectedId == -1 && adVM.vehicleAdsDurations.isNotEmpty) { if (adVM.vehicleAdDurationId.selectedId == -1 &&
adVM.vehicleAdsDurations.isNotEmpty) {
SelectionModel selection = SelectionModel( SelectionModel selection = SelectionModel(
selectedId: adVM.vehicleAdsDurations.first.id ?? 0, 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(), itemPrice: adVM.vehicleAdsDurations.first.price!.toString(),
currency: adVM.vehicleAdsDurations.first.currency!.toString(), currency:
adVM.vehicleAdsDurations.first.currency!.toString(),
); );
adVM.updateVehicleAdDurationId(selection); 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, backgroundColor: MyColors.darkPrimaryColor,
child: const Icon(Icons.add, color: MyColors.white), child: const Icon(Icons.add, color: MyColors.white),
).toViewOnly(context, onTap: () { ).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) { void cancelRequestBottomSheet(BuildContext context, int requestId) {
return actionConfirmationBottomSheet( return actionConfirmationBottomSheet(
context: context, 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(), subtitle: LocaleKeys.requestPermanentlyCancelled.tr(),
actionButtonYes: Expanded( actionButtonYes: Expanded(
child: ShowFillButton( child: ShowFillButton(
@ -41,7 +43,10 @@ class MyRequestsFragment extends StatelessWidget {
final requestVM = context.read<RequestsVM>(); 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) { if (status) {
await requestVM.getRequests(isNeedToRebuild: true); await requestVM.getRequests(isNeedToRebuild: true);
} }
@ -69,7 +74,8 @@ class MyRequestsFragment extends StatelessWidget {
return false; return false;
} }
if (requestStatusEnum == RequestStatusEnum.pending || requestStatusEnum == RequestStatusEnum.submitted) { if (requestStatusEnum == RequestStatusEnum.pending ||
requestStatusEnum == RequestStatusEnum.submitted) {
return true; return true;
} }
return false; return false;
@ -77,7 +83,8 @@ class MyRequestsFragment extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Consumer(builder: (BuildContext context, RequestsVM requestsVM, Widget? child) { return Consumer(
builder: (BuildContext context, RequestsVM requestsVM, Widget? child) {
return Scaffold( return Scaffold(
appBar: CustomAppBar( appBar: CustomAppBar(
title: LocaleKeys.manageRequests.tr(), title: LocaleKeys.manageRequests.tr(),
@ -86,7 +93,9 @@ class MyRequestsFragment extends StatelessWidget {
if (AppState().currentAppType == AppType.provider) ...[ if (AppState().currentAppType == AppType.provider) ...[
Padding( Padding(
padding: const EdgeInsets.only(top: 8, bottom: 8, right: 15), 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, borderRadius: 80,
borderColor: MyColors.lightGreyEFColor, borderColor: MyColors.lightGreyEFColor,
isEnabledBorder: true, isEnabledBorder: true,
@ -96,21 +105,24 @@ class MyRequestsFragment extends StatelessWidget {
}) })
], ],
Padding( 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( child: Badge(
isLabelVisible: requestsVM.requestsFiltersCounter > 0, isLabelVisible: requestsVM.requestsFiltersCounter > 0,
largeSize: 15, largeSize: 15,
smallSize: 20, smallSize: 20,
backgroundColor: MyColors.darkPrimaryColor, backgroundColor: MyColors.darkPrimaryColor,
label: Text('${requestsVM.requestsFiltersCounter}'), label: Text('${requestsVM.requestsFiltersCounter}'),
child: MyAssets.searchIcon.buildSvg(), child: MyAssets.searchIcon.buildSvg(height: 22, width: 22),
), ),
).onPress(() { ).onPress(() {
navigateWithName(context, AppRoutes.requestsFilterView); navigateWithName(context, AppRoutes.requestsFilterView);
}), }),
], ],
).toViewOnly(context, onTap: () { ).toViewOnly(context, onTap: () {
navigateWithName(context, AppRoutes.loginWithPassword, arguments: false); navigateWithName(context, AppRoutes.loginWithPassword,
arguments: false);
}), }),
body: Container( body: Container(
color: MyColors.backgroundColor, color: MyColors.backgroundColor,
@ -122,7 +134,8 @@ class MyRequestsFragment extends StatelessWidget {
FiltersList( FiltersList(
filterList: requestsVM.requestsTypeFilterOptions, filterList: requestsVM.requestsTypeFilterOptions,
onFilterTapped: (index, selectedFilterId) { onFilterTapped: (index, selectedFilterId) {
requestsVM.applyFilterOnRequestsVM(requestsTypeEnum: selectedFilterId.toRequestTypeEnum()); requestsVM.applyFilterOnRequestsVM(
requestsTypeEnum: selectedFilterId.toRequestTypeEnum());
}, },
), ),
// Show condition filters for all request types // Show condition filters for all request types
@ -131,27 +144,31 @@ class MyRequestsFragment extends StatelessWidget {
FiltersList( FiltersList(
filterList: requestsVM.sparePartsConditionFilterOptions, filterList: requestsVM.sparePartsConditionFilterOptions,
onFilterTapped: (index, selectedFilterId) { onFilterTapped: (index, selectedFilterId) {
requestsVM.applySparePartsConditionFilter(conditionId: selectedFilterId); requestsVM.applySparePartsConditionFilter(
conditionId: selectedFilterId);
}, },
), ),
], ],
8.height, 8.height,
Expanded( Expanded(
child: RefreshIndicator( child: RefreshIndicator(
onRefresh: () async => await requestsVM.getRequestsBasedOnFilters(), onRefresh: () async =>
await requestsVM.getRequestsBasedOnFilters(),
child: requestsVM.state == ViewState.busy child: requestsVM.state == ViewState.busy
? const RequestsListShimmer(itemCount: 5) ? const RequestsListShimmer(itemCount: 5)
: requestsVM.myFilteredRequests.isEmpty : requestsVM.myFilteredRequests.isEmpty
? Column( ? Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
LocaleKeys.noRequeststoShow.tr().toText(fontSize: 16, color: MyColors.lightTextColor), LocaleKeys.noRequeststoShow.tr().toText(
fontSize: 16, color: MyColors.lightTextColor),
if (requestsVM.requestsFiltersCounter > 0) ...[ if (requestsVM.requestsFiltersCounter > 0) ...[
8.height, 8.height,
InkWell( InkWell(
onTap: () async { onTap: () async {
requestsVM.clearRequestsFilters(); requestsVM.clearRequestsFilters();
await requestsVM.getRequestsBasedOnFilters(); await requestsVM
.getRequestsBasedOnFilters();
}, },
child: LocaleKeys.clearFilters.tr().toText( child: LocaleKeys.clearFilters.tr().toText(
fontSize: 14, fontSize: 14,
@ -170,43 +187,76 @@ class MyRequestsFragment extends StatelessWidget {
secondaryBackground: Container( secondaryBackground: Container(
color: const Color(0xFFDD0505), color: const Color(0xFFDD0505),
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment:
CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: [ 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, dragStartBehavior: DragStartBehavior.start,
direction: shouldDismiss(requestsVM.myFilteredRequests[index].requestStatus) ? DismissDirection.endToStart : DismissDirection.none, direction: shouldDismiss(requestsVM
confirmDismiss: (DismissDirection direction) async { .myFilteredRequests[index]
cancelRequestBottomSheet(context, requestsVM.myFilteredRequests[index].id); .requestStatus)
? DismissDirection.endToStart
: DismissDirection.none,
confirmDismiss:
(DismissDirection direction) async {
cancelRequestBottomSheet(context,
requestsVM.myFilteredRequests[index].id);
return; return;
}, },
child: RequestItem( child: RequestItem(
request: requestsVM.myFilteredRequests[index], request:
requestsVM.myFilteredRequests[index],
shouldShowStatuses: true, shouldShowStatuses: true,
onTap: () async { onTap: () async {
RequestModel request = requestsVM.myFilteredRequests[index]; RequestModel request =
requestsVM.updateCurrentSelectedRequest(request); requestsVM.myFilteredRequests[index];
requestsVM.updateCurrentSelectedRequest(
request);
log("request.requestStatus: ${request.requestStatus}"); log("request.requestStatus: ${request.requestStatus}");
if (request.requestStatus == RequestStatusEnum.pending || if (request.requestStatus ==
request.requestStatus == RequestStatusEnum.cancelled || RequestStatusEnum.pending ||
request.requestStatus == RequestStatusEnum.expired) { request.requestStatus ==
RequestStatusEnum.cancelled ||
request.requestStatus ==
RequestStatusEnum.expired) {
return; return;
} }
if (AppState().currentAppType == AppType.provider) { if (AppState().currentAppType ==
RequestDetailPageArguments requestDetailPageArguments = RequestDetailPageArguments(requestIndex: index, requestModel: request); AppType.provider) {
navigateWithName(context, AppRoutes.requestsDetailPage, arguments: requestDetailPageArguments); RequestDetailPageArguments
requestDetailPageArguments =
RequestDetailPageArguments(
requestIndex: index,
requestModel: request);
navigateWithName(context,
AppRoutes.requestsDetailPage,
arguments:
requestDetailPageArguments);
} else { } else {
requestsVM.myFilteredRequests[index].offerCount = 0; requestsVM.myFilteredRequests[index]
.offerCount = 0;
requestsVM.notifyListeners(); requestsVM.notifyListeners();
navigateWithName(context, AppRoutes.offersListPage, arguments: request.id); navigateWithName(
context, AppRoutes.offersListPage,
arguments: request.id);
} }
}).toViewOnly(context, onTap: () { }).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; return 16.height;
}, },
itemCount: requestsVM.myFilteredRequests.length, 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: AppState().currentAppType == AppType.customer
? FloatingActionButton( ? FloatingActionButton(
onPressed: () => navigateWithName(context, AppRoutes.createRequestPage), onPressed: () =>
navigateWithName(context, AppRoutes.createRequestPage),
backgroundColor: MyColors.darkPrimaryColor, backgroundColor: MyColors.darkPrimaryColor,
child: const Icon( child: const Icon(
Icons.add, Icons.add,
color: MyColors.white, color: MyColors.white,
), ),
).toViewOnly(context, onTap: () { ).toViewOnly(context, onTap: () {
navigateWithName(context, AppRoutes.loginWithPassword, arguments: false); navigateWithName(context, AppRoutes.loginWithPassword,
arguments: false);
}) })
: null, : null,
); );

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

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

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

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

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

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

Loading…
Cancel
Save