Completed the Enhancement CR

faiz_development_common
Faiz Hashmi 1 day ago
parent 3146a9433b
commit 6f6533a1ca

@ -901,6 +901,6 @@
"downloadApp": "تحميل تطبيق موّاتر:", "downloadApp": "تحميل تطبيق موّاتر:",
"androidApp": "أندرويد", "androidApp": "أندرويد",
"iosApp": "آيفون", "iosApp": "آيفون",
"pleaseWait": "يرجى الانتظار..." "pleaseWait": "يرجى الانتظار...",
"numberOfShipping": "عدد الشحنات"
} }

@ -899,5 +899,6 @@
"downloadApp": "Download MoWater App:", "downloadApp": "Download MoWater App:",
"androidApp": "Android", "androidApp": "Android",
"iosApp": "iOS", "iosApp": "iOS",
"pleaseWait": "Please wait..." "pleaseWait": "Please wait...",
"numberOfShipping": "Number of Shipping"
} }

@ -380,6 +380,9 @@ class MyAssets {
static String whatsAppIcon = "${assetPath}icons/whatsapp_icon.svg"; static String whatsAppIcon = "${assetPath}icons/whatsapp_icon.svg";
static const String arrowRight = "${assetPath}icons/ic_arrow_right.svg"; static const String arrowRight = "${assetPath}icons/ic_arrow_right.svg";
static const String brokenImage = "${assetPath}images/broken_image.png"; static const String brokenImage = "${assetPath}images/broken_image.png";
// Tones
static const String engineStartSound = "${assetPath}tones/engine_start_tone.mp3";
} }
RegExp numReg = RegExp(r".*[0-9].*"); RegExp numReg = RegExp(r".*[0-9].*");
@ -471,4 +474,3 @@ class GuestConsts {
}, },
); );
} }

@ -441,6 +441,7 @@ class CodegenLoader extends AssetLoader{
"usedSparePart": "قطع غيار مستعملة", "usedSparePart": "قطع غيار مستعملة",
"hasYourCarHadAccident": "هل تعرضت سيارتك لأي حادث؟", "hasYourCarHadAccident": "هل تعرضت سيارتك لأي حادث؟",
"odometerReading": "قراءة عداد المسافات (كم)", "odometerReading": "قراءة عداد المسافات (كم)",
"canMileageIncrease": "هل يمكن زيادة المسافة المقطوعة عند الفحص؟",
"currentlyInUse": "قيد الاستخدام حاليًا", "currentlyInUse": "قيد الاستخدام حاليًا",
"demand": "الطلب", "demand": "الطلب",
"adDurationExpired": "انتهت مدة عرض الإعلان الخاص بك", "adDurationExpired": "انتهت مدة عرض الإعلان الخاص بك",
@ -916,7 +917,8 @@ class CodegenLoader extends AssetLoader{
"downloadApp": "تحميل تطبيق موّاتر:", "downloadApp": "تحميل تطبيق موّاتر:",
"androidApp": "أندرويد", "androidApp": "أندرويد",
"iosApp": "آيفون", "iosApp": "آيفون",
"pleaseWait": "يرجى الانتظار..." "pleaseWait": "يرجى الانتظار...",
"numberOfShipping": "عدد الشحنات"
}; };
static const Map<String,dynamic> _en_US = { static const Map<String,dynamic> _en_US = {
"firstTimeLogIn": "First Time Log In", "firstTimeLogIn": "First Time Log In",
@ -1346,6 +1348,7 @@ static const Map<String,dynamic> _en_US = {
"usedSparePart": "Used Spare Part", "usedSparePart": "Used Spare Part",
"hasYourCarHadAccident": "Has your car had any accident?", "hasYourCarHadAccident": "Has your car had any accident?",
"odometerReading": "Odometer Reading (Km)", "odometerReading": "Odometer Reading (Km)",
"canMileageIncrease": "Can the mileage be increased when checking?",
"currentlyInUse": "Currently in Use", "currentlyInUse": "Currently in Use",
"demand": "Demand", "demand": "Demand",
"adDurationExpired": "Your Ad Duration time is over", "adDurationExpired": "Your Ad Duration time is over",
@ -1818,7 +1821,8 @@ static const Map<String,dynamic> _en_US = {
"downloadApp": "Download MoWater App:", "downloadApp": "Download MoWater App:",
"androidApp": "Android", "androidApp": "Android",
"iosApp": "iOS", "iosApp": "iOS",
"pleaseWait": "Please wait..." "pleaseWait": "Please wait...",
"numberOfShipping": "Number of Shipping"
}; };
static const Map<String, Map<String,dynamic>> mapLocales = {"ar_SA": _ar_SA, "en_US": _en_US}; static const Map<String, Map<String,dynamic>> mapLocales = {"ar_SA": _ar_SA, "en_US": _en_US};
} }

@ -406,6 +406,7 @@ abstract class LocaleKeys {
static const usedSparePart = 'usedSparePart'; static const usedSparePart = 'usedSparePart';
static const hasYourCarHadAccident = 'hasYourCarHadAccident'; static const hasYourCarHadAccident = 'hasYourCarHadAccident';
static const odometerReading = 'odometerReading'; static const odometerReading = 'odometerReading';
static const canMileageIncrease = 'canMileageIncrease';
static const currentlyInUse = 'currentlyInUse'; static const currentlyInUse = 'currentlyInUse';
static const demand = 'demand'; static const demand = 'demand';
static const adDurationExpired = 'adDurationExpired'; static const adDurationExpired = 'adDurationExpired';
@ -882,5 +883,6 @@ abstract class LocaleKeys {
static const androidApp = 'androidApp'; static const androidApp = 'androidApp';
static const iosApp = 'iosApp'; static const iosApp = 'iosApp';
static const pleaseWait = 'pleaseWait'; static const pleaseWait = 'pleaseWait';
static const numberOfShipping = 'numberOfShipping';
} }

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

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

@ -237,6 +237,7 @@ class AdVM extends BaseVM {
bool hasMoreDataForExploreAds = true; bool hasMoreDataForExploreAds = true;
bool hasMoreDataForMyAds = true; bool hasMoreDataForMyAds = true;
bool isLoadingMore = false; bool isLoadingMore = false;
bool isLoadingAdsDuration = false;
Future<void> fetchMoreExploreAds() async { Future<void> fetchMoreExploreAds() async {
if (isLoadingMore) return; if (isLoadingMore) return;
@ -457,9 +458,18 @@ class AdVM extends BaseVM {
} }
Future<void> getVehicleAdsDuration() async { Future<void> getVehicleAdsDuration() async {
isLoadingAdsDuration = true;
notifyListeners();
try {
final countryId = AppState().getUser.data!.userInfo!.countryId; final countryId = AppState().getUser.data!.userInfo!.countryId;
vehicleAdsDurations = await adsRepo.getAdsDuration(countryId: countryId); vehicleAdsDurations = await adsRepo.getAdsDuration(countryId: countryId);
isLoadingAdsDuration = false;
notifyListeners(); notifyListeners();
} catch (e) {
isLoadingAdsDuration = false;
notifyListeners();
rethrow;
}
} }
Future<void> getVehicleAdsSpecialServices({required int cityId, required int countryId, required int specialServiceType}) async { Future<void> getVehicleAdsSpecialServices({required int cityId, required int countryId, required int specialServiceType}) async {

@ -35,6 +35,20 @@ class ChatVM extends BaseVM {
HubConnection? hubConnection; HubConnection? hubConnection;
bool _isSubscribed = false; // Track if listeners are subscribed bool _isSubscribed = false; // Track if listeners are subscribed
// Store ViewModels to avoid context dependency
RequestsVM? _requestsVM;
AdVM? _adVM;
// Track if a message is currently being sent (prevents duplicate sends)
bool isSendingMessage = false;
// Method to inject ViewModels (called once from Dashboard)
void setViewModels({required RequestsVM requestsVM, required AdVM adVM}) {
_requestsVM = requestsVM;
_adVM = adVM;
logger.i("ViewModels injected into ChatVM");
}
String chatMessageText = ""; String chatMessageText = "";
updateChatMessageText(String value) { updateChatMessageText(String value) {
@ -251,7 +265,12 @@ class ChatVM extends BaseVM {
notifyListeners(); notifyListeners();
} }
void _subscribeToReceiveRequestOfferMessages(BuildContext context) { void _subscribeToReceiveRequestOfferMessages() {
if (_requestsVM == null) {
logger.e("RequestsVM not set! Cannot subscribe to request offer messages.");
return;
}
hubConnection!.on(SignalrConsts.receiveMessageRequestOffer, (List<Object?>? arguments) { hubConnection!.on(SignalrConsts.receiveMessageRequestOffer, (List<Object?>? arguments) {
logger.i(arguments); logger.i(arguments);
@ -261,9 +280,10 @@ class ChatVM extends BaseVM {
final chatMessage = ChatMessageModel.fromJson(message as Map<String, dynamic>, isForReqOfferImagesURLs: true); final chatMessage = ChatMessageModel.fromJson(message as Map<String, dynamic>, isForReqOfferImagesURLs: true);
chat.add(chatMessage); chat.add(chatMessage);
} }
onNewMessageReceivedForRequestOffer(messages: chat, requestsVM: context.read<RequestsVM>()); onNewMessageReceivedForRequestOffer(messages: chat, requestsVM: _requestsVM!);
// Utils.showToast(arguments.toString()); // Utils.showToast(arguments.toString());
}); });
logger.i("Subscribed to RequestOffer messages");
} }
Future<bool> markMessagesAsRead(BuildContext context, List<int> messageIds, ChatTypeEnum chatTypeEnum) async { Future<bool> markMessagesAsRead(BuildContext context, List<int> messageIds, ChatTypeEnum chatTypeEnum) async {
@ -346,13 +366,15 @@ class ChatVM extends BaseVM {
// Clear old listeners before subscribing (prevents duplicates) // Clear old listeners before subscribing (prevents duplicates)
_clearAllListeners(); _clearAllListeners();
// Subscribe to messages (only if context is available for first-time connection) // Subscribe to messages (no context needed anymore!)
// For reconnections, we'll re-subscribe without context if (_requestsVM != null && _adVM != null) {
if (context != null) { _subscribeToReceiveRequestOfferMessages();
_subscribeToReceiveRequestOfferMessages(context); _subscribeToReceiveAdMessages();
_subscribeToReceiveAdMessages(context); _subscribeToReceiveGeneralMessages();
_subscribeToReceiveGeneralMessages(context);
_isSubscribed = true; _isSubscribed = true;
logger.i("All listeners subscribed successfully");
} else {
logger.w("ViewModels not injected yet. Listeners will be subscribed after injection.");
} }
// Configure handlers // Configure handlers
@ -367,11 +389,21 @@ class ChatVM extends BaseVM {
_isSubscribed = false; _isSubscribed = false;
}); });
hubConnection!.onreconnected((connectionId) { hubConnection!.onreconnected((connectionId) async {
logger.i("onReconnected: ${connectionId.toString()}"); logger.i("onReconnected: ${connectionId.toString()}");
// Re-subscribe after automatic reconnect (SignalR's own reconnect, not ours)
// This is safe because we clear listeners in onreconnecting
_retryCount = 0; // Reset on successful reconnect _retryCount = 0; // Reset on successful reconnect
// CRITICAL FIX: Re-subscribe listeners after automatic reconnect
if (_requestsVM != null && _adVM != null) {
_clearAllListeners();
_subscribeToReceiveRequestOfferMessages();
_subscribeToReceiveAdMessages();
_subscribeToReceiveGeneralMessages();
_isSubscribed = true;
logger.i("✅ Listeners re-subscribed after automatic reconnection");
} else {
logger.e("❌ Cannot re-subscribe: ViewModels not set!");
}
}); });
} catch (e) { } catch (e) {
logger.e("Error during hub connection setup: ${e.toString()}"); logger.e("Error during hub connection setup: ${e.toString()}");
@ -527,6 +559,16 @@ class ChatVM extends BaseVM {
}) async { }) async {
if (chatMessageType == ChatMessageTypeEnum.freeText && message.isEmpty) return false; if (chatMessageType == ChatMessageTypeEnum.freeText && message.isEmpty) return false;
// Prevent duplicate sends
if (isSendingMessage) {
logger.w("Message already being sent, ignoring duplicate tap");
return false;
}
isSendingMessage = true;
notifyListeners();
try {
// Ensure connection is established // Ensure connection is established
if (hubConnection == null || hubConnection!.state != HubConnectionState.connected) { if (hubConnection == null || hubConnection!.state != HubConnectionState.connected) {
logger.i("Hub Not Connected!! Attempting to connect..."); logger.i("Hub Not Connected!! Attempting to connect...");
@ -538,6 +580,8 @@ class ChatVM extends BaseVM {
// Check again after attempt // Check again after attempt
if (hubConnection == null || hubConnection!.state != HubConnectionState.connected) { if (hubConnection == null || hubConnection!.state != HubConnectionState.connected) {
Utils.showToast("Failed to connect. Please try again."); Utils.showToast("Failed to connect. Please try again.");
isSendingMessage = false;
notifyListeners();
return false; return false;
} }
} }
@ -590,21 +634,38 @@ class ChatVM extends BaseVM {
serviceProviderOffersList[providerIndex].chatMessages!.add(chatMessageModel); serviceProviderOffersList[providerIndex].chatMessages!.add(chatMessageModel);
} }
} else { } else {
int providerIndex = context.read<RequestsVM>().myFilteredRequests.indexWhere((element) => (element.customerUserID == receiverId && element.id == requestId)); // Use stored RequestsVM instead of context.read
if (_requestsVM != null) {
int providerIndex = _requestsVM!.myFilteredRequests.indexWhere((element) => (element.customerUserID == receiverId && element.id == requestId));
log("providerIndex2:$providerIndex"); log("providerIndex2:$providerIndex");
if (providerIndex != -1) { if (providerIndex != -1) {
context.read<RequestsVM>().addChatMessagesInRequestsModel(msg: chatMessageModel, index: providerIndex); _requestsVM!.addChatMessagesInRequestsModel(msg: chatMessageModel, index: providerIndex);
}
} }
} }
isSendingMessage = false;
notifyListeners();
return true; return true;
} catch (e) { } catch (e) {
logger.e("error in invoking SendMessage: ${e.toString()}"); logger.e("error in invoking SendMessage: ${e.toString()}");
Utils.showToast("Failed to send message"); Utils.showToast("Failed to send message");
isSendingMessage = false;
notifyListeners();
return false; return false;
} }
} }
isSendingMessage = false;
notifyListeners();
return false;
} catch (e) {
logger.e("Unexpected error in onMessageSendForRequest: ${e.toString()}");
isSendingMessage = false;
notifyListeners();
return false; return false;
} }
}
List<ServiceProvidersOffers> serviceProviderOffersList = []; List<ServiceProvidersOffers> serviceProviderOffersList = [];
@ -806,7 +867,12 @@ class ChatVM extends BaseVM {
} }
} }
void _subscribeToReceiveAdMessages(BuildContext context) { void _subscribeToReceiveAdMessages() {
if (_adVM == null) {
logger.e("AdVM not set! Cannot subscribe to ad messages.");
return;
}
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 = [];
@ -818,6 +884,7 @@ class ChatVM extends BaseVM {
logger.i(arguments); logger.i(arguments);
// Utils.showToast(arguments.toString()); // Utils.showToast(arguments.toString());
}); });
logger.i("Subscribed to Ads messages");
} }
Future<void> onNewMessageReceivedForAds({required List<ChatMessageModel> messages}) async { Future<void> onNewMessageReceivedForAds({required List<ChatMessageModel> messages}) async {
@ -837,6 +904,16 @@ class ChatVM extends BaseVM {
}) async { }) async {
if (message.isEmpty) return false; if (message.isEmpty) return false;
// Prevent duplicate sends
if (isSendingMessage) {
logger.w("Message already being sent, ignoring duplicate tap");
return false;
}
isSendingMessage = true;
notifyListeners();
try {
// Ensure connection is established // Ensure connection is established
if (hubConnection == null || hubConnection!.state != HubConnectionState.connected) { if (hubConnection == null || hubConnection!.state != HubConnectionState.connected) {
logger.i("Hub Not Connected!! Attempting to connect..."); logger.i("Hub Not Connected!! Attempting to connect...");
@ -848,6 +925,8 @@ class ChatVM extends BaseVM {
// Check again after attempt // Check again after attempt
if (hubConnection == null || hubConnection!.state != HubConnectionState.connected) { if (hubConnection == null || hubConnection!.state != HubConnectionState.connected) {
Utils.showToast("Failed to connect. Please try again."); Utils.showToast("Failed to connect. Please try again.");
isSendingMessage = false;
notifyListeners();
return false; return false;
} }
} }
@ -886,14 +965,28 @@ class ChatVM extends BaseVM {
if (AppState().currentAppType == AppType.customer) { if (AppState().currentAppType == AppType.customer) {
} else {} } else {}
isSendingMessage = false;
notifyListeners();
return true; return true;
} catch (e) { } catch (e) {
logger.e("error in invoking SendMessage: ${e.toString()}"); logger.e("error in invoking SendMessage: ${e.toString()}");
Utils.showToast("Failed to send message"); Utils.showToast("Failed to send message");
isSendingMessage = false;
notifyListeners();
return false; return false;
} }
} }
isSendingMessage = false;
notifyListeners();
return false; return false;
} catch (e) {
logger.e("Unexpected error in onTextMessageSendForAds: ${e.toString()}");
isSendingMessage = false;
notifyListeners();
return false;
}
} }
// ========================General ================== // ========================General ==================
@ -934,7 +1027,8 @@ class ChatVM extends BaseVM {
notifyListeners(); notifyListeners();
} }
void _subscribeToReceiveGeneralMessages(BuildContext context) { void _subscribeToReceiveGeneralMessages() {
// 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;
List<ChatMessageModel> chat = []; List<ChatMessageModel> chat = [];
@ -946,6 +1040,7 @@ class ChatVM extends BaseVM {
logger.i(arguments); logger.i(arguments);
// Utils.showToast(arguments.toString()); // Utils.showToast(arguments.toString());
}); });
logger.i("Subscribed to General messages");
} }
Future<bool> onTextMessageSendForGeneralChat({ Future<bool> onTextMessageSendForGeneralChat({
@ -957,6 +1052,16 @@ class ChatVM extends BaseVM {
}) async { }) async {
if (message.isEmpty) return false; if (message.isEmpty) return false;
// Prevent duplicate sends
if (isSendingMessage) {
logger.w("Message already being sent, ignoring duplicate tap");
return false;
}
isSendingMessage = true;
notifyListeners();
try {
// Ensure connection is established // Ensure connection is established
if (hubConnection == null || hubConnection!.state != HubConnectionState.connected) { if (hubConnection == null || hubConnection!.state != HubConnectionState.connected) {
logger.i("Hub Not Connected!! Attempting to connect..."); logger.i("Hub Not Connected!! Attempting to connect...");
@ -968,6 +1073,8 @@ class ChatVM extends BaseVM {
// Check again after attempt // Check again after attempt
if (hubConnection == null || hubConnection!.state != HubConnectionState.connected) { if (hubConnection == null || hubConnection!.state != HubConnectionState.connected) {
Utils.showToast("Failed to connect. Please try again."); Utils.showToast("Failed to connect. Please try again.");
isSendingMessage = false;
notifyListeners();
return false; return false;
} }
} }
@ -1003,13 +1110,27 @@ class ChatVM extends BaseVM {
if (AppState().currentAppType == AppType.customer) { if (AppState().currentAppType == AppType.customer) {
} else {} } else {}
isSendingMessage = false;
notifyListeners();
return true; return true;
} catch (e) { } catch (e) {
logger.e("error in invoking SendMessage: ${e.toString()}"); logger.e("error in invoking SendMessage: ${e.toString()}");
Utils.showToast("Failed to send message"); Utils.showToast("Failed to send message");
isSendingMessage = false;
notifyListeners();
return false; return false;
} }
} }
isSendingMessage = false;
notifyListeners();
return false; return false;
} catch (e) {
logger.e("Unexpected error in onTextMessageSendForGeneralChat: ${e.toString()}");
isSendingMessage = false;
notifyListeners();
return false;
}
} }
} }

@ -107,6 +107,9 @@ class DashboardVmCustomer extends BaseVM with WidgetsBindingObserver {
RequestsVM requestsVM = Provider.of<RequestsVM>(context, listen: false); RequestsVM requestsVM = Provider.of<RequestsVM>(context, listen: false);
ChatVM chatVM = Provider.of<ChatVM>(context, listen: false); ChatVM chatVM = Provider.of<ChatVM>(context, listen: false);
// Inject ViewModels into ChatVM (only needs to be done once, but safe to repeat)
chatVM.setViewModels(requestsVM: requestsVM, adVM: adVM);
await appointmentsVM.populateBranchesFilterList(); await appointmentsVM.populateBranchesFilterList();
await appointmentsVM.populateAppointmentsFilterList(); await appointmentsVM.populateAppointmentsFilterList();
await adVM.populateAdsFilterList(); await adVM.populateAdsFilterList();

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

@ -85,6 +85,7 @@ 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}");
// TODO: VERIFY THIS WITH ZAHOOR // TODO: VERIFY THIS WITH ZAHOOR
if (!isPaid) { if (!isPaid) {
// Utils.showToast(LocaleKeys.paymentFailed.tr()); // Utils.showToast(LocaleKeys.paymentFailed.tr());
@ -95,6 +96,7 @@ class PaymentVM extends ChangeNotifier {
} }
onVerified(); onVerified();
} catch (e) { } catch (e) {
log("verifyPayments: ${e.toString()}");
Utils.showToast(e.toString()); Utils.showToast(e.toString());
return; return;
} }
@ -206,6 +208,7 @@ class PaymentVM extends ChangeNotifier {
} }
void onSubscriptionPaymentSuccess(BuildContext context) { void onSubscriptionPaymentSuccess(BuildContext context) {
log("onSubscriptionPaymentSuccess");
pop(context); pop(context);
// 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); navigateReplaceWithNameUntilRoute(context, AppRoutes.dashboard);

@ -391,8 +391,8 @@ class RequestsVM extends BaseVM {
// Initialize spare parts condition filters (All, New, Used) // Initialize spare parts condition filters (All, New, Used)
if (sparePartsConditionFilterOptions.isEmpty) { if (sparePartsConditionFilterOptions.isEmpty) {
sparePartsConditionFilterOptions.add(FilterListModel(title: "All", isSelected: true, id: 3)); // All sparePartsConditionFilterOptions.add(FilterListModel(title: "All", isSelected: true, id: 3)); // All
sparePartsConditionFilterOptions.add(FilterListModel(title: "New Spare Part", isSelected: false, id: 1)); // New sparePartsConditionFilterOptions.add(FilterListModel(title: "New", isSelected: false, id: 1)); // New
sparePartsConditionFilterOptions.add(FilterListModel(title: "Used Spare Part", isSelected: false, id: 2)); // Used sparePartsConditionFilterOptions.add(FilterListModel(title: "Used", isSelected: false, id: 2)); // Used
} }
if (vehicleBrands.isEmpty) { if (vehicleBrands.isEmpty) {

@ -30,11 +30,16 @@ class ShippingManagementVM extends BaseVM {
List<FilterListModel> shippingRequestStatusesList = []; List<FilterListModel> shippingRequestStatusesList = [];
String requestStatusComments = ""; String requestStatusComments = "";
String numberOfShipping = "";
void updateRequestStatusComments(var value) { void updateRequestStatusComments(var value) {
requestStatusComments = value; requestStatusComments = value;
} }
void updateNumberOfShipping(var value) {
numberOfShipping = value;
}
bool isSelfPickupTapped = false; bool isSelfPickupTapped = false;
void updateIsSelfPickupTapped(var value) { void updateIsSelfPickupTapped(var value) {
@ -55,6 +60,7 @@ class ShippingManagementVM extends BaseVM {
} }
selfPickupRequestsFilterOptions[0].isSelected = true; selfPickupRequestsFilterOptions[0].isSelected = true;
requestStatusComments = ""; requestStatusComments = "";
numberOfShipping = "";
} }
Future<void> populateSelfPickupRequestFilterList() async { Future<void> populateSelfPickupRequestFilterList() async {
@ -174,10 +180,16 @@ class ShippingManagementVM extends BaseVM {
Future<bool> onUpdateShippingStatusTapped({required BuildContext context, required ShippingRequestStatusEnum shippingStatusEnum, required int shippingRequestId}) async { Future<bool> onUpdateShippingStatusTapped({required BuildContext context, required ShippingRequestStatusEnum shippingStatusEnum, required int shippingRequestId}) async {
Utils.showLoading(context); Utils.showLoading(context);
try { try {
GenericRespModel? genericRespModel = await shippingRepo.updateShippingRequestStatus(shippingStatusEnum: shippingStatusEnum, shippingRequestId: shippingRequestId, comment: requestStatusComments); GenericRespModel? genericRespModel = await shippingRepo.updateShippingRequestStatus(
shippingStatusEnum: shippingStatusEnum,
shippingRequestId: shippingRequestId,
comment: requestStatusComments,
numberOfShipping: numberOfShipping.isEmpty ? null : numberOfShipping,
);
Utils.showToast(genericRespModel.message.toString()); Utils.showToast(genericRespModel.message.toString());
Utils.hideLoading(context); Utils.hideLoading(context);
requestStatusComments = ""; requestStatusComments = "";
numberOfShipping = "";
return genericRespModel.messageStatus == 1; return genericRespModel.messageStatus == 1;
} catch (e) { } catch (e) {
logger.i(e.toString()); logger.i(e.toString());
@ -190,11 +202,16 @@ class ShippingManagementVM extends BaseVM {
Future<bool> onUpdateSelfPickupStatusTapped({required BuildContext context, required SelfPickupRequestStatusEnum selfPickupRequestStatusEnum, required int shippingRequestId}) async { Future<bool> onUpdateSelfPickupStatusTapped({required BuildContext context, required SelfPickupRequestStatusEnum selfPickupRequestStatusEnum, required int shippingRequestId}) async {
Utils.showLoading(context); Utils.showLoading(context);
try { try {
GenericRespModel? genericRespModel = GenericRespModel? genericRespModel = await shippingRepo.updateSelfPickupRequestStatus(
await shippingRepo.updateSelfPickupRequestStatus(selfPickupStatusEnum: selfPickupRequestStatusEnum, shippingRequestId: shippingRequestId, comment: requestStatusComments); selfPickupStatusEnum: selfPickupRequestStatusEnum,
shippingRequestId: shippingRequestId,
comment: requestStatusComments,
numberOfShipping: numberOfShipping.isEmpty ? null : numberOfShipping,
);
Utils.showToast(genericRespModel.message.toString()); Utils.showToast(genericRespModel.message.toString());
Utils.hideLoading(context); Utils.hideLoading(context);
requestStatusComments = ""; requestStatusComments = "";
numberOfShipping = "";
return genericRespModel.messageStatus == 1; return genericRespModel.messageStatus == 1;
} catch (e) { } catch (e) {
logger.i(e.toString()); logger.i(e.toString());

@ -174,8 +174,8 @@ class _AdCardState extends State<AdCard> {
children: [ children: [
ClipRRect( ClipRRect(
child: SizedBox( child: SizedBox(
width: 100, width: 80,
height: 100, height: 80,
child: hasMultipleImages child: hasMultipleImages
? PageView.builder( ? PageView.builder(
controller: _pageController, controller: _pageController,
@ -328,8 +328,8 @@ class _AdCardState extends State<AdCard> {
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
children: [ children: [
(widget.adDetails.vehicle!.cityName ?? "").toText(color: MyColors.lightTextColor), (widget.adDetails.vehicle!.cityName ?? "").toText(color: MyColors.lightTextColor, fontSize: 8),
widget.adDetails.createdOn != null ? DateTime.parse(widget.adDetails.createdOn!).getTimeAgo().toText(color: MyColors.lightTextColor) : const SizedBox(), widget.adDetails.createdOn != null ? DateTime.parse(widget.adDetails.createdOn!).getTimeAgo().toText(color: MyColors.lightTextColor, fontSize: 8) : const SizedBox(),
], ],
), ),
], ],

@ -307,7 +307,16 @@ class _ChatViewState extends State<ChatView> {
], ],
Expanded( Expanded(
flex: 1, flex: 1,
child: const Icon(Icons.send_rounded, color: MyColors.darkPrimaryColor, size: 30).onPress( child: chatVM.isSendingMessage
? const SizedBox(
width: 30,
height: 30,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(MyColors.darkPrimaryColor),
),
)
: const Icon(Icons.send_rounded, color: MyColors.darkPrimaryColor, size: 30).onPress(
() async { () async {
ChatMessageTypeEnum chatMessageTypeEnum = ChatMessageTypeEnum.freeText; ChatMessageTypeEnum chatMessageTypeEnum = ChatMessageTypeEnum.freeText;

@ -62,7 +62,9 @@ class AdsFragment extends StatelessWidget {
).toViewOnly(context, onTap: () { ).toViewOnly(context, onTap: () {
navigateWithName(context, AppRoutes.loginWithPassword, arguments: false); navigateWithName(context, AppRoutes.loginWithPassword, arguments: false);
}), }),
body: SizedBox( body: Stack(
children: [
SizedBox(
width: double.infinity, width: double.infinity,
height: double.infinity, height: double.infinity,
child: Column( child: Column(
@ -177,9 +179,30 @@ class AdsFragment extends StatelessWidget {
], ],
), ),
), ),
// Loading overlay when FAB is clicked and API is fetching
if (adVM.isLoadingAdsDuration)
Container(
width: double.infinity,
height: double.infinity,
color: Colors.black.withOpacity(0.3),
child: const Center(
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(MyColors.darkPrimaryColor),
),
),
),
],
),
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton(
onPressed: () async { onPressed: () async {
adVM.getVehicleAdsDuration(); // Await API calls with loading state
await adVM.getVehicleAdsDuration();
// Ensure vehicle types are loaded
if (adVM.vehicleTypes.isEmpty) {
await adVM.getVehicleTypes();
}
if (AppState().userType != UserType.providerDealer) { if (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(

@ -86,9 +86,9 @@ class RequestItem extends StatelessWidget {
), ),
], ],
2.height, 2.height,
request.cityName.toText(color: MyColors.lightTextColor, letterSpacing: -0.4, fontSize: 10), request.cityName.toText(color: MyColors.lightTextColor, letterSpacing: -0.4, fontSize: 6),
if (request.createdOn != null) ...[ if (request.createdOn != null) ...[
DateTime.parse(request.createdOn!).getTimeAgo().toText(color: MyColors.lightTextColor, letterSpacing: -0.4, fontSize: 10), DateTime.parse(request.createdOn!).getTimeAgo().toText(color: MyColors.lightTextColor, letterSpacing: -0.4, fontSize: 6),
], ],
], ],
) )

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

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

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

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

Loading…
Cancel
Save