diff --git a/assets/langs/ar-SA.json b/assets/langs/ar-SA.json index a7bf966..efa3119 100644 --- a/assets/langs/ar-SA.json +++ b/assets/langs/ar-SA.json @@ -901,6 +901,6 @@ "downloadApp": "تحميل تطبيق موّاتر:", "androidApp": "أندرويد", "iosApp": "آيفون", - "pleaseWait": "يرجى الانتظار..." - + "pleaseWait": "يرجى الانتظار...", + "numberOfShipping": "عدد الشحنات" } \ No newline at end of file diff --git a/assets/langs/en-US.json b/assets/langs/en-US.json index 0afc648..e288afd 100644 --- a/assets/langs/en-US.json +++ b/assets/langs/en-US.json @@ -899,5 +899,6 @@ "downloadApp": "Download MoWater App:", "androidApp": "Android", "iosApp": "iOS", - "pleaseWait": "Please wait..." + "pleaseWait": "Please wait...", + "numberOfShipping": "Number of Shipping" } \ No newline at end of file diff --git a/assets/tones/engine_start_tone.mp3 b/assets/tones/engine_start_tone.mp3 new file mode 100644 index 0000000..184be22 Binary files /dev/null and b/assets/tones/engine_start_tone.mp3 differ diff --git a/lib/classes/consts.dart b/lib/classes/consts.dart index 600ebb9..95d3937 100644 --- a/lib/classes/consts.dart +++ b/lib/classes/consts.dart @@ -380,6 +380,9 @@ class MyAssets { static String whatsAppIcon = "${assetPath}icons/whatsapp_icon.svg"; static const String arrowRight = "${assetPath}icons/ic_arrow_right.svg"; static const String brokenImage = "${assetPath}images/broken_image.png"; + + // Tones + static const String engineStartSound = "${assetPath}tones/engine_start_tone.mp3"; } RegExp numReg = RegExp(r".*[0-9].*"); @@ -471,4 +474,3 @@ class GuestConsts { }, ); } - diff --git a/lib/generated/codegen_loader.g.dart b/lib/generated/codegen_loader.g.dart index 3f62126..1a2c357 100644 --- a/lib/generated/codegen_loader.g.dart +++ b/lib/generated/codegen_loader.g.dart @@ -441,6 +441,7 @@ class CodegenLoader extends AssetLoader{ "usedSparePart": "قطع غيار مستعملة", "hasYourCarHadAccident": "هل تعرضت سيارتك لأي حادث؟", "odometerReading": "قراءة عداد المسافات (كم)", + "canMileageIncrease": "هل يمكن زيادة المسافة المقطوعة عند الفحص؟", "currentlyInUse": "قيد الاستخدام حاليًا", "demand": "الطلب", "adDurationExpired": "انتهت مدة عرض الإعلان الخاص بك", @@ -916,7 +917,8 @@ class CodegenLoader extends AssetLoader{ "downloadApp": "تحميل تطبيق موّاتر:", "androidApp": "أندرويد", "iosApp": "آيفون", - "pleaseWait": "يرجى الانتظار..." + "pleaseWait": "يرجى الانتظار...", + "numberOfShipping": "عدد الشحنات" }; static const Map _en_US = { "firstTimeLogIn": "First Time Log In", @@ -1346,6 +1348,7 @@ static const Map _en_US = { "usedSparePart": "Used Spare Part", "hasYourCarHadAccident": "Has your car had any accident?", "odometerReading": "Odometer Reading (Km)", + "canMileageIncrease": "Can the mileage be increased when checking?", "currentlyInUse": "Currently in Use", "demand": "Demand", "adDurationExpired": "Your Ad Duration time is over", @@ -1818,7 +1821,8 @@ static const Map _en_US = { "downloadApp": "Download MoWater App:", "androidApp": "Android", "iosApp": "iOS", - "pleaseWait": "Please wait..." + "pleaseWait": "Please wait...", + "numberOfShipping": "Number of Shipping" }; static const Map> mapLocales = {"ar_SA": _ar_SA, "en_US": _en_US}; } diff --git a/lib/generated/locale_keys.g.dart b/lib/generated/locale_keys.g.dart index eae7b40..fa241f8 100644 --- a/lib/generated/locale_keys.g.dart +++ b/lib/generated/locale_keys.g.dart @@ -406,6 +406,7 @@ abstract class LocaleKeys { static const usedSparePart = 'usedSparePart'; static const hasYourCarHadAccident = 'hasYourCarHadAccident'; static const odometerReading = 'odometerReading'; + static const canMileageIncrease = 'canMileageIncrease'; static const currentlyInUse = 'currentlyInUse'; static const demand = 'demand'; static const adDurationExpired = 'adDurationExpired'; @@ -882,5 +883,6 @@ abstract class LocaleKeys { static const androidApp = 'androidApp'; static const iosApp = 'iosApp'; static const pleaseWait = 'pleaseWait'; + static const numberOfShipping = 'numberOfShipping'; } diff --git a/lib/models/shipping_models/shipping_status_model.dart b/lib/models/shipping_models/shipping_status_model.dart index 6ee1ff4..4e36f70 100644 --- a/lib/models/shipping_models/shipping_status_model.dart +++ b/lib/models/shipping_models/shipping_status_model.dart @@ -15,6 +15,7 @@ class ShippingRequestModel { String? customerName; int? vehicleTypeID; String? vehicleType; + String? numberOfShipping; ShippingRequestModel({ this.id, @@ -28,6 +29,7 @@ class ShippingRequestModel { this.customerName, this.vehicleTypeID, this.vehicleType, + this.numberOfShipping, }); ShippingRequestModel.fromJsonShipping(Map json, bool isForShipping) { @@ -45,6 +47,7 @@ class ShippingRequestModel { customerName = json['customerName']; vehicleTypeID = json['vehicleTypeID']; vehicleType = json['vehicleType']; + numberOfShipping = json['numberOfShipping']; } } diff --git a/lib/repositories/shipping_repo.dart b/lib/repositories/shipping_repo.dart index bf9c2b9..6f4a837 100644 --- a/lib/repositories/shipping_repo.dart +++ b/lib/repositories/shipping_repo.dart @@ -18,9 +18,9 @@ abstract class ShippingRepo { Future> getShippingRequestListByStatus({ShippingRequestStatusEnum? shippingStatusEnum, int? requestId}); - Future updateShippingRequestStatus({required ShippingRequestStatusEnum shippingStatusEnum, required int shippingRequestId, String? comment}); + Future updateShippingRequestStatus({required ShippingRequestStatusEnum shippingStatusEnum, required int shippingRequestId, String? comment, String? numberOfShipping}); - Future updateSelfPickupRequestStatus({required SelfPickupRequestStatusEnum selfPickupStatusEnum, required int shippingRequestId, String? comment}); + Future updateSelfPickupRequestStatus({required SelfPickupRequestStatusEnum selfPickupStatusEnum, required int shippingRequestId, String? comment, String? numberOfShipping}); } class ShippingRepoImp extends ShippingRepo { @@ -78,12 +78,13 @@ class ShippingRepoImp extends ShippingRepo { } @override - Future updateShippingRequestStatus({required ShippingRequestStatusEnum shippingStatusEnum, required int shippingRequestId, String? comment}) async { + Future updateShippingRequestStatus({required ShippingRequestStatusEnum shippingStatusEnum, required int shippingRequestId, String? comment, String? numberOfShipping}) async { String token = appState.getUser.data!.accessToken ?? ""; Map queryParameters = { "id": "$shippingRequestId", "shippingStatus": "${shippingStatusEnum.getIdFromShippingStatusEnum()}", "comment": comment ?? "", + "numberOfShipping": numberOfShipping ?? "", }; GenericRespModel genericRespModel = await apiClient.postJsonForObject( @@ -101,12 +102,13 @@ class ShippingRepoImp extends ShippingRepo { } @override - Future updateSelfPickupRequestStatus({required SelfPickupRequestStatusEnum selfPickupStatusEnum, required int shippingRequestId, String? comment}) async { + Future updateSelfPickupRequestStatus({required SelfPickupRequestStatusEnum selfPickupStatusEnum, required int shippingRequestId, String? comment, String? numberOfShipping}) async { String token = appState.getUser.data!.accessToken ?? ""; Map queryParameters = { "id": "$shippingRequestId", "selfPickUpStatus": "${selfPickupStatusEnum.getIdFromSelfPickupStatusEnum()}", "comment": comment ?? "", + "numberOfShipping": numberOfShipping ?? "", }; GenericRespModel genericRespModel = await apiClient.postJsonForObject( diff --git a/lib/view_models/ad_view_model.dart b/lib/view_models/ad_view_model.dart index 4e9bcac..aa1c82f 100644 --- a/lib/view_models/ad_view_model.dart +++ b/lib/view_models/ad_view_model.dart @@ -237,6 +237,7 @@ class AdVM extends BaseVM { bool hasMoreDataForExploreAds = true; bool hasMoreDataForMyAds = true; bool isLoadingMore = false; + bool isLoadingAdsDuration = false; Future fetchMoreExploreAds() async { if (isLoadingMore) return; @@ -457,9 +458,18 @@ class AdVM extends BaseVM { } Future getVehicleAdsDuration() async { - final countryId = AppState().getUser.data!.userInfo!.countryId; - vehicleAdsDurations = await adsRepo.getAdsDuration(countryId: countryId); + isLoadingAdsDuration = true; notifyListeners(); + try { + final countryId = AppState().getUser.data!.userInfo!.countryId; + vehicleAdsDurations = await adsRepo.getAdsDuration(countryId: countryId); + isLoadingAdsDuration = false; + notifyListeners(); + } catch (e) { + isLoadingAdsDuration = false; + notifyListeners(); + rethrow; + } } Future getVehicleAdsSpecialServices({required int cityId, required int countryId, required int specialServiceType}) async { diff --git a/lib/view_models/chat_view_model.dart b/lib/view_models/chat_view_model.dart index c075154..24b2206 100644 --- a/lib/view_models/chat_view_model.dart +++ b/lib/view_models/chat_view_model.dart @@ -35,6 +35,20 @@ class ChatVM extends BaseVM { HubConnection? hubConnection; bool _isSubscribed = false; // Track if listeners are subscribed + // Store ViewModels to avoid context dependency + RequestsVM? _requestsVM; + AdVM? _adVM; + + // Track if a message is currently being sent (prevents duplicate sends) + bool isSendingMessage = false; + + // Method to inject ViewModels (called once from Dashboard) + void setViewModels({required RequestsVM requestsVM, required AdVM adVM}) { + _requestsVM = requestsVM; + _adVM = adVM; + logger.i("ViewModels injected into ChatVM"); + } + String chatMessageText = ""; updateChatMessageText(String value) { @@ -251,7 +265,12 @@ class ChatVM extends BaseVM { notifyListeners(); } - void _subscribeToReceiveRequestOfferMessages(BuildContext context) { + void _subscribeToReceiveRequestOfferMessages() { + if (_requestsVM == null) { + logger.e("RequestsVM not set! Cannot subscribe to request offer messages."); + return; + } + hubConnection!.on(SignalrConsts.receiveMessageRequestOffer, (List? arguments) { logger.i(arguments); @@ -261,9 +280,10 @@ class ChatVM extends BaseVM { final chatMessage = ChatMessageModel.fromJson(message as Map, isForReqOfferImagesURLs: true); chat.add(chatMessage); } - onNewMessageReceivedForRequestOffer(messages: chat, requestsVM: context.read()); + onNewMessageReceivedForRequestOffer(messages: chat, requestsVM: _requestsVM!); // Utils.showToast(arguments.toString()); }); + logger.i("Subscribed to RequestOffer messages"); } Future markMessagesAsRead(BuildContext context, List messageIds, ChatTypeEnum chatTypeEnum) async { @@ -346,13 +366,15 @@ class ChatVM extends BaseVM { // Clear old listeners before subscribing (prevents duplicates) _clearAllListeners(); - // Subscribe to messages (only if context is available for first-time connection) - // For reconnections, we'll re-subscribe without context - if (context != null) { - _subscribeToReceiveRequestOfferMessages(context); - _subscribeToReceiveAdMessages(context); - _subscribeToReceiveGeneralMessages(context); + // Subscribe to messages (no context needed anymore!) + if (_requestsVM != null && _adVM != null) { + _subscribeToReceiveRequestOfferMessages(); + _subscribeToReceiveAdMessages(); + _subscribeToReceiveGeneralMessages(); _isSubscribed = true; + logger.i("All listeners subscribed successfully"); + } else { + logger.w("ViewModels not injected yet. Listeners will be subscribed after injection."); } // Configure handlers @@ -367,11 +389,21 @@ class ChatVM extends BaseVM { _isSubscribed = false; }); - hubConnection!.onreconnected((connectionId) { + hubConnection!.onreconnected((connectionId) async { logger.i("onReconnected: ${connectionId.toString()}"); - // Re-subscribe after automatic reconnect (SignalR's own reconnect, not ours) - // This is safe because we clear listeners in onreconnecting _retryCount = 0; // Reset on successful reconnect + + // ✅ CRITICAL FIX: Re-subscribe listeners after automatic reconnect + if (_requestsVM != null && _adVM != null) { + _clearAllListeners(); + _subscribeToReceiveRequestOfferMessages(); + _subscribeToReceiveAdMessages(); + _subscribeToReceiveGeneralMessages(); + _isSubscribed = true; + logger.i("✅ Listeners re-subscribed after automatic reconnection"); + } else { + logger.e("❌ Cannot re-subscribe: ViewModels not set!"); + } }); } catch (e) { logger.e("Error during hub connection setup: ${e.toString()}"); @@ -527,83 +559,112 @@ class ChatVM extends BaseVM { }) async { if (chatMessageType == ChatMessageTypeEnum.freeText && message.isEmpty) return false; - // Ensure connection is established - if (hubConnection == null || hubConnection!.state != HubConnectionState.connected) { - logger.i("Hub Not Connected!! Attempting to connect..."); - await buildHubConnection(context: context); + // ✅ Prevent duplicate sends + if (isSendingMessage) { + logger.w("Message already being sent, ignoring duplicate tap"); + return false; + } - // Wait a bit for connection to establish - await Future.delayed(const Duration(milliseconds: 500)); + isSendingMessage = true; + notifyListeners(); - // Check again after attempt + try { + // Ensure connection is established if (hubConnection == null || hubConnection!.state != HubConnectionState.connected) { - Utils.showToast("Failed to connect. Please try again."); - return false; - } - } - - if (hubConnection!.state == HubConnectionState.connected) { - final userId = AppState().getUser.data!.userInfo!.userId.toString(); - final name = AppState().getUser.data!.userInfo!.firstName.toString(); - List> requestImagesMap = []; - List messageImages = getMessagesImageList(); - if (messageImages.isNotEmpty && chatMessageType == ChatMessageTypeEnum.image) { - for (var element in messageImages) { - requestImagesMap.add(element.toJson()); + logger.i("Hub Not Connected!! Attempting to connect..."); + await buildHubConnection(context: context); + + // Wait a bit for connection to establish + await Future.delayed(const Duration(milliseconds: 500)); + + // Check again after attempt + if (hubConnection == null || hubConnection!.state != HubConnectionState.connected) { + Utils.showToast("Failed to connect. Please try again."); + isSendingMessage = false; + notifyListeners(); + return false; } } - final obj = { - "ReceiverUserID": receiverId, - "SenderUserID": userId, - "MessageType": chatMessageType.getIdFromChatMessageTypeEnum(), - "ChatText": chatMessageType == ChatMessageTypeEnum.image ? "${messageImages.length} Image(s)" : message, - "RequestID": requestId, - "ReqOfferID": latestOfferId, - }; - - if (chatMessageType == ChatMessageTypeEnum.image) { - obj.addAll({"ReqOfferImages": requestImagesMap}); - } - logger.i(obj); - - try { - await hubConnection!.invoke( - SignalrConsts.sendMessageRequestOffer, - args: [obj], - ); - ChatMessageModel chatMessageModel = ChatMessageModel( - messageType: chatMessageType.getIdFromChatMessageTypeEnum(), - chatText: message, - isMyMessage: true, - requestID: requestId, - senderName: name, - senderUserID: userId, - chatMessageTypeEnum: chatMessageType, - receiverUserID: receiverId, - messageImages: messageImages, - ); - - if (AppState().currentAppType == AppType.customer) { - int providerIndex = serviceProviderOffersList.indexWhere((element) => element.providerUserId == receiverId); - if (providerIndex != -1) { - serviceProviderOffersList[providerIndex].chatMessages!.add(chatMessageModel); + if (hubConnection!.state == HubConnectionState.connected) { + final userId = AppState().getUser.data!.userInfo!.userId.toString(); + final name = AppState().getUser.data!.userInfo!.firstName.toString(); + List> requestImagesMap = []; + List messageImages = getMessagesImageList(); + if (messageImages.isNotEmpty && chatMessageType == ChatMessageTypeEnum.image) { + for (var element in messageImages) { + requestImagesMap.add(element.toJson()); } - } else { - int providerIndex = context.read().myFilteredRequests.indexWhere((element) => (element.customerUserID == receiverId && element.id == requestId)); - log("providerIndex2:$providerIndex"); - if (providerIndex != -1) { - context.read().addChatMessagesInRequestsModel(msg: chatMessageModel, index: providerIndex); + } + final obj = { + "ReceiverUserID": receiverId, + "SenderUserID": userId, + "MessageType": chatMessageType.getIdFromChatMessageTypeEnum(), + "ChatText": chatMessageType == ChatMessageTypeEnum.image ? "${messageImages.length} Image(s)" : message, + "RequestID": requestId, + "ReqOfferID": latestOfferId, + }; + + if (chatMessageType == ChatMessageTypeEnum.image) { + obj.addAll({"ReqOfferImages": requestImagesMap}); + } + logger.i(obj); + + try { + await hubConnection!.invoke( + SignalrConsts.sendMessageRequestOffer, + args: [obj], + ); + + ChatMessageModel chatMessageModel = ChatMessageModel( + messageType: chatMessageType.getIdFromChatMessageTypeEnum(), + chatText: message, + isMyMessage: true, + requestID: requestId, + senderName: name, + senderUserID: userId, + chatMessageTypeEnum: chatMessageType, + receiverUserID: receiverId, + messageImages: messageImages, + ); + + if (AppState().currentAppType == AppType.customer) { + int providerIndex = serviceProviderOffersList.indexWhere((element) => element.providerUserId == receiverId); + if (providerIndex != -1) { + serviceProviderOffersList[providerIndex].chatMessages!.add(chatMessageModel); + } + } else { + // ✅ Use stored RequestsVM instead of context.read + if (_requestsVM != null) { + int providerIndex = _requestsVM!.myFilteredRequests.indexWhere((element) => (element.customerUserID == receiverId && element.id == requestId)); + log("providerIndex2:$providerIndex"); + if (providerIndex != -1) { + _requestsVM!.addChatMessagesInRequestsModel(msg: chatMessageModel, index: providerIndex); + } + } } + + isSendingMessage = false; + notifyListeners(); + return true; + } catch (e) { + logger.e("error in invoking SendMessage: ${e.toString()}"); + Utils.showToast("Failed to send message"); + isSendingMessage = false; + notifyListeners(); + return false; } - return true; - } catch (e) { - logger.e("error in invoking SendMessage: ${e.toString()}"); - Utils.showToast("Failed to send message"); - return false; } + + isSendingMessage = false; + notifyListeners(); + return false; + } catch (e) { + logger.e("Unexpected error in onMessageSendForRequest: ${e.toString()}"); + isSendingMessage = false; + notifyListeners(); + return false; } - return false; } List 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? arguments) { if (arguments == null || arguments.isEmpty) return; List chat = []; @@ -818,6 +884,7 @@ class ChatVM extends BaseVM { logger.i(arguments); // Utils.showToast(arguments.toString()); }); + logger.i("Subscribed to Ads messages"); } Future onNewMessageReceivedForAds({required List messages}) async { @@ -837,63 +904,89 @@ class ChatVM extends BaseVM { }) async { if (message.isEmpty) return false; - // Ensure connection is established - if (hubConnection == null || hubConnection!.state != HubConnectionState.connected) { - logger.i("Hub Not Connected!! Attempting to connect..."); - await buildHubConnection(context: context); + // ✅ Prevent duplicate sends + if (isSendingMessage) { + logger.w("Message already being sent, ignoring duplicate tap"); + return false; + } - // Wait a bit for connection to establish - await Future.delayed(const Duration(milliseconds: 500)); + isSendingMessage = true; + notifyListeners(); - // Check again after attempt + try { + // Ensure connection is established if (hubConnection == null || hubConnection!.state != HubConnectionState.connected) { - Utils.showToast("Failed to connect. Please try again."); - return false; + logger.i("Hub Not Connected!! Attempting to connect..."); + await buildHubConnection(context: context); + + // Wait a bit for connection to establish + await Future.delayed(const Duration(milliseconds: 500)); + + // Check again after attempt + if (hubConnection == null || hubConnection!.state != HubConnectionState.connected) { + Utils.showToast("Failed to connect. Please try again."); + isSendingMessage = false; + notifyListeners(); + return false; + } } - } - - if (hubConnection!.state == HubConnectionState.connected) { - final userId = AppState().getUser.data!.userInfo!.userId.toString(); - final name = AppState().getUser.data!.userInfo!.firstName.toString(); - final obj = { - "ReceiverUserID": receiverId, - // "SenderUserID": userId, - "MessageType": chatMessageType.getIdFromChatMessageTypeEnum(), - "ChatText": message, - "AdsID": adId, - }; - logger.i("$obj"); - - try { - await hubConnection!.invoke( - SignalrConsts.sendMessageAds, - args: [obj], - ); - ChatMessageModel chatMessageModel = ChatMessageModel( - messageType: chatMessageType.getIdFromChatMessageTypeEnum(), - chatText: message, - isMyMessage: true, - senderName: name, - senderUserID: userId, - chatMessageTypeEnum: ChatMessageTypeEnum.freeText, - receiverUserID: receiverId, - ); - - currentMessagesForAds.add(chatMessageModel); - - notifyListeners(); - - if (AppState().currentAppType == AppType.customer) { - } else {} - return true; - } catch (e) { - logger.e("error in invoking SendMessage: ${e.toString()}"); - Utils.showToast("Failed to send message"); - return false; + if (hubConnection!.state == HubConnectionState.connected) { + final userId = AppState().getUser.data!.userInfo!.userId.toString(); + final name = AppState().getUser.data!.userInfo!.firstName.toString(); + final obj = { + "ReceiverUserID": receiverId, + // "SenderUserID": userId, + "MessageType": chatMessageType.getIdFromChatMessageTypeEnum(), + "ChatText": message, + "AdsID": adId, + }; + logger.i("$obj"); + + try { + await hubConnection!.invoke( + SignalrConsts.sendMessageAds, + args: [obj], + ); + + ChatMessageModel chatMessageModel = ChatMessageModel( + messageType: chatMessageType.getIdFromChatMessageTypeEnum(), + chatText: message, + isMyMessage: true, + senderName: name, + senderUserID: userId, + chatMessageTypeEnum: ChatMessageTypeEnum.freeText, + receiverUserID: receiverId, + ); + + currentMessagesForAds.add(chatMessageModel); + + notifyListeners(); + + if (AppState().currentAppType == AppType.customer) { + } else {} + + isSendingMessage = false; + notifyListeners(); + return true; + } catch (e) { + logger.e("error in invoking SendMessage: ${e.toString()}"); + Utils.showToast("Failed to send message"); + isSendingMessage = false; + notifyListeners(); + return false; + } } + + isSendingMessage = false; + notifyListeners(); + return false; + } catch (e) { + logger.e("Unexpected error in onTextMessageSendForAds: ${e.toString()}"); + isSendingMessage = false; + notifyListeners(); + return false; } - return false; } // ========================General ================== @@ -934,7 +1027,8 @@ class ChatVM extends BaseVM { notifyListeners(); } - void _subscribeToReceiveGeneralMessages(BuildContext context) { + void _subscribeToReceiveGeneralMessages() { + // General messages don't need specific VM, just storing messages hubConnection!.on(SignalrConsts.receiveMessageGeneral, (List? arguments) { if (arguments == null || arguments.isEmpty) return; List chat = []; @@ -946,6 +1040,7 @@ class ChatVM extends BaseVM { logger.i(arguments); // Utils.showToast(arguments.toString()); }); + logger.i("Subscribed to General messages"); } Future onTextMessageSendForGeneralChat({ @@ -957,59 +1052,85 @@ class ChatVM extends BaseVM { }) async { if (message.isEmpty) return false; - // Ensure connection is established - if (hubConnection == null || hubConnection!.state != HubConnectionState.connected) { - logger.i("Hub Not Connected!! Attempting to connect..."); - await buildHubConnection(context: context); + // ✅ Prevent duplicate sends + if (isSendingMessage) { + logger.w("Message already being sent, ignoring duplicate tap"); + return false; + } - // Wait a bit for connection to establish - await Future.delayed(const Duration(milliseconds: 500)); + isSendingMessage = true; + notifyListeners(); - // Check again after attempt + try { + // Ensure connection is established if (hubConnection == null || hubConnection!.state != HubConnectionState.connected) { - Utils.showToast("Failed to connect. Please try again."); - return false; + logger.i("Hub Not Connected!! Attempting to connect..."); + await buildHubConnection(context: context); + + // Wait a bit for connection to establish + await Future.delayed(const Duration(milliseconds: 500)); + + // Check again after attempt + if (hubConnection == null || hubConnection!.state != HubConnectionState.connected) { + Utils.showToast("Failed to connect. Please try again."); + isSendingMessage = false; + notifyListeners(); + return false; + } } - } - - if (hubConnection!.state == HubConnectionState.connected) { - final userId = AppState().getUser.data!.userInfo!.userId.toString(); - final name = AppState().getUser.data!.userInfo!.firstName.toString(); - final obj = { - "ReceiverUserID": receiverId, - "MessageType": chatMessageType.getIdFromChatMessageTypeEnum(), - "ChatText": message, - }; - logger.i("$obj"); - try { - await hubConnection!.invoke( - SignalrConsts.sendMessageGeneral, - args: [obj], - ); - - ChatMessageModel chatMessageModel = ChatMessageModel( - messageType: chatMessageType.getIdFromChatMessageTypeEnum(), - chatText: message, - isMyMessage: true, - senderName: name, - senderUserID: userId, - chatMessageTypeEnum: ChatMessageTypeEnum.freeText, - receiverUserID: receiverId, - ); - - currentMessagesForGeneralChat.add(chatMessageModel); - notifyListeners(); - - if (AppState().currentAppType == AppType.customer) { - } else {} - return true; - } catch (e) { - logger.e("error in invoking SendMessage: ${e.toString()}"); - Utils.showToast("Failed to send message"); - return false; + if (hubConnection!.state == HubConnectionState.connected) { + final userId = AppState().getUser.data!.userInfo!.userId.toString(); + final name = AppState().getUser.data!.userInfo!.firstName.toString(); + final obj = { + "ReceiverUserID": receiverId, + "MessageType": chatMessageType.getIdFromChatMessageTypeEnum(), + "ChatText": message, + }; + logger.i("$obj"); + + try { + await hubConnection!.invoke( + SignalrConsts.sendMessageGeneral, + args: [obj], + ); + + ChatMessageModel chatMessageModel = ChatMessageModel( + messageType: chatMessageType.getIdFromChatMessageTypeEnum(), + chatText: message, + isMyMessage: true, + senderName: name, + senderUserID: userId, + chatMessageTypeEnum: ChatMessageTypeEnum.freeText, + receiverUserID: receiverId, + ); + + currentMessagesForGeneralChat.add(chatMessageModel); + notifyListeners(); + + if (AppState().currentAppType == AppType.customer) { + } else {} + + isSendingMessage = false; + notifyListeners(); + return true; + } catch (e) { + logger.e("error in invoking SendMessage: ${e.toString()}"); + Utils.showToast("Failed to send message"); + isSendingMessage = false; + notifyListeners(); + return false; + } } + + isSendingMessage = false; + notifyListeners(); + return false; + } catch (e) { + logger.e("Unexpected error in onTextMessageSendForGeneralChat: ${e.toString()}"); + isSendingMessage = false; + notifyListeners(); + return false; } - return false; } } diff --git a/lib/view_models/dashboard_view_model_customer.dart b/lib/view_models/dashboard_view_model_customer.dart index abdf321..a184209 100644 --- a/lib/view_models/dashboard_view_model_customer.dart +++ b/lib/view_models/dashboard_view_model_customer.dart @@ -107,6 +107,9 @@ class DashboardVmCustomer extends BaseVM with WidgetsBindingObserver { RequestsVM requestsVM = Provider.of(context, listen: false); ChatVM chatVM = Provider.of(context, listen: false); + // ✅ Inject ViewModels into ChatVM (only needs to be done once, but safe to repeat) + chatVM.setViewModels(requestsVM: requestsVM, adVM: adVM); + await appointmentsVM.populateBranchesFilterList(); await appointmentsVM.populateAppointmentsFilterList(); await adVM.populateAdsFilterList(); diff --git a/lib/view_models/dashboard_view_model_provider.dart b/lib/view_models/dashboard_view_model_provider.dart index 1d02098..9da5397 100644 --- a/lib/view_models/dashboard_view_model_provider.dart +++ b/lib/view_models/dashboard_view_model_provider.dart @@ -122,6 +122,10 @@ class DashboardVMProvider extends BaseVM with WidgetsBindingObserver { final serviceVM = context.read(); final adVM = context.read(); final subscriptionsVM = context.read(); + + // ✅ Inject ViewModels into ChatVM (only needs to be done once, but safe to repeat) + chatVM.setViewModels(requestsVM: requestsVM, adVM: adVM); + await requestsVM.populateDataForRequestsFilter(); await requestsVM.populateDeliveryOptionEnums(); await appointmentVM.populateAppointmentsFilterList(); diff --git a/lib/view_models/payment_view_model.dart b/lib/view_models/payment_view_model.dart index 41b0717..373f964 100644 --- a/lib/view_models/payment_view_model.dart +++ b/lib/view_models/payment_view_model.dart @@ -85,6 +85,7 @@ class PaymentVM extends ChangeNotifier { ); // Utils.hideLoading(context); // Commented out: matching pair with showLoading + log("isPaid: ${isPaid}"); // TODO: VERIFY THIS WITH ZAHOOR if (!isPaid) { // Utils.showToast(LocaleKeys.paymentFailed.tr()); @@ -95,6 +96,7 @@ class PaymentVM extends ChangeNotifier { } onVerified(); } catch (e) { + log("verifyPayments: ${e.toString()}"); Utils.showToast(e.toString()); return; } @@ -206,6 +208,7 @@ class PaymentVM extends ChangeNotifier { } void onSubscriptionPaymentSuccess(BuildContext context) { + log("onSubscriptionPaymentSuccess"); pop(context); // context.read().getSubscriptionBySP(AppState().getUser.data?.userInfo?.providerId.toString() ?? "", true); navigateReplaceWithNameUntilRoute(context, AppRoutes.dashboard); diff --git a/lib/view_models/requests_view_model.dart b/lib/view_models/requests_view_model.dart index a1639d2..5a77eb5 100644 --- a/lib/view_models/requests_view_model.dart +++ b/lib/view_models/requests_view_model.dart @@ -391,8 +391,8 @@ class RequestsVM extends BaseVM { // Initialize spare parts condition filters (All, New, Used) if (sparePartsConditionFilterOptions.isEmpty) { 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: "Used Spare Part", isSelected: false, id: 2)); // Used + sparePartsConditionFilterOptions.add(FilterListModel(title: "New", isSelected: false, id: 1)); // New + sparePartsConditionFilterOptions.add(FilterListModel(title: "Used", isSelected: false, id: 2)); // Used } if (vehicleBrands.isEmpty) { diff --git a/lib/view_models/shipping_management_view_model.dart b/lib/view_models/shipping_management_view_model.dart index 446faa9..2697868 100644 --- a/lib/view_models/shipping_management_view_model.dart +++ b/lib/view_models/shipping_management_view_model.dart @@ -30,11 +30,16 @@ class ShippingManagementVM extends BaseVM { List shippingRequestStatusesList = []; String requestStatusComments = ""; + String numberOfShipping = ""; void updateRequestStatusComments(var value) { requestStatusComments = value; } + void updateNumberOfShipping(var value) { + numberOfShipping = value; + } + bool isSelfPickupTapped = false; void updateIsSelfPickupTapped(var value) { @@ -55,6 +60,7 @@ class ShippingManagementVM extends BaseVM { } selfPickupRequestsFilterOptions[0].isSelected = true; requestStatusComments = ""; + numberOfShipping = ""; } Future populateSelfPickupRequestFilterList() async { @@ -174,10 +180,16 @@ class ShippingManagementVM extends BaseVM { Future onUpdateShippingStatusTapped({required BuildContext context, required ShippingRequestStatusEnum shippingStatusEnum, required int shippingRequestId}) async { Utils.showLoading(context); try { - GenericRespModel? genericRespModel = await shippingRepo.updateShippingRequestStatus(shippingStatusEnum: shippingStatusEnum, shippingRequestId: shippingRequestId, comment: requestStatusComments); + GenericRespModel? genericRespModel = await shippingRepo.updateShippingRequestStatus( + shippingStatusEnum: shippingStatusEnum, + shippingRequestId: shippingRequestId, + comment: requestStatusComments, + numberOfShipping: numberOfShipping.isEmpty ? null : numberOfShipping, + ); Utils.showToast(genericRespModel.message.toString()); Utils.hideLoading(context); requestStatusComments = ""; + numberOfShipping = ""; return genericRespModel.messageStatus == 1; } catch (e) { logger.i(e.toString()); @@ -190,11 +202,16 @@ class ShippingManagementVM extends BaseVM { Future onUpdateSelfPickupStatusTapped({required BuildContext context, required SelfPickupRequestStatusEnum selfPickupRequestStatusEnum, required int shippingRequestId}) async { Utils.showLoading(context); try { - GenericRespModel? genericRespModel = - await shippingRepo.updateSelfPickupRequestStatus(selfPickupStatusEnum: selfPickupRequestStatusEnum, shippingRequestId: shippingRequestId, comment: requestStatusComments); + GenericRespModel? genericRespModel = await shippingRepo.updateSelfPickupRequestStatus( + selfPickupStatusEnum: selfPickupRequestStatusEnum, + shippingRequestId: shippingRequestId, + comment: requestStatusComments, + numberOfShipping: numberOfShipping.isEmpty ? null : numberOfShipping, + ); Utils.showToast(genericRespModel.message.toString()); Utils.hideLoading(context); requestStatusComments = ""; + numberOfShipping = ""; return genericRespModel.messageStatus == 1; } catch (e) { logger.i(e.toString()); diff --git a/lib/views/advertisement/components/ads_list_widget.dart b/lib/views/advertisement/components/ads_list_widget.dart index ee59eef..ac33d3b 100644 --- a/lib/views/advertisement/components/ads_list_widget.dart +++ b/lib/views/advertisement/components/ads_list_widget.dart @@ -174,8 +174,8 @@ class _AdCardState extends State { children: [ ClipRRect( child: SizedBox( - width: 100, - height: 100, + width: 80, + height: 80, child: hasMultipleImages ? PageView.builder( controller: _pageController, @@ -328,8 +328,8 @@ class _AdCardState extends State { crossAxisAlignment: CrossAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.start, children: [ - (widget.adDetails.vehicle!.cityName ?? "").toText(color: MyColors.lightTextColor), - widget.adDetails.createdOn != null ? DateTime.parse(widget.adDetails.createdOn!).getTimeAgo().toText(color: MyColors.lightTextColor) : const SizedBox(), + (widget.adDetails.vehicle!.cityName ?? "").toText(color: MyColors.lightTextColor, fontSize: 8), + widget.adDetails.createdOn != null ? DateTime.parse(widget.adDetails.createdOn!).getTimeAgo().toText(color: MyColors.lightTextColor, fontSize: 8) : const SizedBox(), ], ), ], diff --git a/lib/views/chat/chat_view.dart b/lib/views/chat/chat_view.dart index 78640d9..011305f 100644 --- a/lib/views/chat/chat_view.dart +++ b/lib/views/chat/chat_view.dart @@ -307,25 +307,34 @@ class _ChatViewState extends State { ], Expanded( flex: 1, - child: const Icon(Icons.send_rounded, color: MyColors.darkPrimaryColor, size: 30).onPress( - () async { - ChatMessageTypeEnum chatMessageTypeEnum = ChatMessageTypeEnum.freeText; + child: chatVM.isSendingMessage + ? const SizedBox( + width: 30, + height: 30, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation(MyColors.darkPrimaryColor), + ), + ) + : const Icon(Icons.send_rounded, color: MyColors.darkPrimaryColor, size: 30).onPress( + () async { + ChatMessageTypeEnum chatMessageTypeEnum = ChatMessageTypeEnum.freeText; - if (chatVM.pickedImagesForMessage.isNotEmpty) { - chatMessageTypeEnum = ChatMessageTypeEnum.image; - } - final status = await onMessageSend(chatMessageType: chatMessageTypeEnum); + if (chatVM.pickedImagesForMessage.isNotEmpty) { + chatMessageTypeEnum = ChatMessageTypeEnum.image; + } + final status = await onMessageSend(chatMessageType: chatMessageTypeEnum); - if (status) { - chatVM.scrollChatDown(); - if (chatMessageTypeEnum == ChatMessageTypeEnum.freeText) { - chatVM.clearChatMessageText(); - } else if (chatMessageTypeEnum == ChatMessageTypeEnum.image) { - chatVM.clearPickedImagesForMessage(); - } - } - }, - ), + if (status) { + chatVM.scrollChatDown(); + if (chatMessageTypeEnum == ChatMessageTypeEnum.freeText) { + chatVM.clearChatMessageText(); + } else if (chatMessageTypeEnum == ChatMessageTypeEnum.image) { + chatVM.clearPickedImagesForMessage(); + } + } + }, + ), ), ], ], diff --git a/lib/views/common_fragments/ads_fragment.dart b/lib/views/common_fragments/ads_fragment.dart index 6e01f16..e65a834 100644 --- a/lib/views/common_fragments/ads_fragment.dart +++ b/lib/views/common_fragments/ads_fragment.dart @@ -62,124 +62,147 @@ class AdsFragment extends StatelessWidget { ).toViewOnly(context, onTap: () { navigateWithName(context, AppRoutes.loginWithPassword, arguments: false); }), - body: SizedBox( - width: double.infinity, - height: double.infinity, - child: Column( - children: [ - 16.height, - Consumer( - builder: (BuildContext context, AdVM adVM, Widget? child) { - return Column( - children: [ - Row( + body: Stack( + children: [ + SizedBox( + width: double.infinity, + height: double.infinity, + child: Column( + children: [ + 16.height, + Consumer( + builder: (BuildContext context, AdVM adVM, Widget? child) { + return Column( children: [ - Expanded( - child: ShowFillButton( - isFilled: adVM.isExploreAdsTapped, - maxHeight: 55, - fontSize: 15, - title: LocaleKeys.exploreAds.tr(), - txtColor: adVM.isExploreAdsTapped ? MyColors.white : MyColors.darkTextColor, - onPressed: () { - log("accessToken: ${AppState().getUser.data!.accessToken}"); - log("AppState().getUser.data!.userInfo!.userId;: ${AppState().getUser.data!.userInfo!.userId}"); + Row( + children: [ + Expanded( + child: ShowFillButton( + isFilled: adVM.isExploreAdsTapped, + maxHeight: 55, + fontSize: 15, + title: LocaleKeys.exploreAds.tr(), + txtColor: adVM.isExploreAdsTapped ? MyColors.white : MyColors.darkTextColor, + onPressed: () { + log("accessToken: ${AppState().getUser.data!.accessToken}"); + log("AppState().getUser.data!.userInfo!.userId;: ${AppState().getUser.data!.userInfo!.userId}"); - if (adVM.myAds.isEmpty) { - adVM.getMyAds(); + if (adVM.myAds.isEmpty) { + adVM.getMyAds(); + } + adVM.updateIsExploreAds(true); + }, + ), + ), + 12.width, + Expanded( + child: ShowFillButton( + isFilled: !adVM.isExploreAdsTapped, + txtColor: !adVM.isExploreAdsTapped ? MyColors.white : MyColors.darkTextColor, + maxHeight: 55, + fontSize: 15, + title: LocaleKeys.myAds.tr(), + onPressed: () { + adVM.updateIsExploreAds(false); + }, + ).toViewOnly(context, onTap: () { + navigateWithName(context, AppRoutes.loginWithPassword, arguments: false); + }), + ), + ], + ).horPaddingMain(), + if (adVM.isExploreAdsTapped) ...[ + if (adVM.adsFiltersCounter == 0) ...[ + 16.height, + FiltersList( + showImages: true, + filterList: adVM.exploreAdsFilterOptions, + onFilterTapped: (index, selectedFilterId) { + adVM.applyFilterOnExploreAds(vehicleBrandId: selectedFilterId); + }, + needLeftPadding: false, + ).paddingOnly(left: 21), + ] + ] else ...[ + 16.height, + FiltersList( + filterList: adVM.myAdsFilterOptions, + onFilterTapped: (index, selectedFilterId) { + adVM.applyFilterOnMyAds(adPostStatusEnum: selectedFilterId.toAdPostEnum()); + }, + needLeftPadding: false, + ).paddingOnly(left: 21), + ], + ], + ); + }, + ), + 16.height, + Expanded( + child: RefreshIndicator( + onRefresh: () async { + if (adVM.isExploreAdsTapped) { + int vehicleBrandId = adVM.exploreAdsFilterOptions.firstWhere((element) => element.isSelected).id; + adVM.applyFilterOnExploreAds(vehicleBrandId: vehicleBrandId); + } else { + AdPostStatus adPostStatusEnum = adVM.myAdsFilterOptions.firstWhere((element) => element.isSelected).id.toAdPostEnum(); + adVM.applyFilterOnMyAds(adPostStatusEnum: adPostStatusEnum); + } + }, + child: adVM.state == ViewState.busy + ? const AdsListShimmer(itemCount: 5) + : AdsListWidget( + isAdsFragment: true, + isDraftAds: false, + shouldShowAdStatus: !adVM.isExploreAdsTapped, + adsList: getAdsList(adVM), + hasMoreData: adVM.isExploreAdsTapped ? adVM.hasMoreDataForExploreAds : adVM.hasMoreDataForMyAds, + onFetchMoreAds: () async { + AdPostStatus adPostStatusEnum = adVM.myAdsFilterOptions.firstWhere((element) => element.isSelected).id.toAdPostEnum(); + + if (adVM.isExploreAdsTapped) { + await adVM.fetchMoreExploreAds(); + } else { + await adVM.fetchMoreMyAds(adsStatus: adPostStatusEnum); } - adVM.updateIsExploreAds(true); }, ), - ), - 12.width, - Expanded( - child: ShowFillButton( - isFilled: !adVM.isExploreAdsTapped, - txtColor: !adVM.isExploreAdsTapped ? MyColors.white : MyColors.darkTextColor, - maxHeight: 55, - fontSize: 15, - title: LocaleKeys.myAds.tr(), - onPressed: () { - adVM.updateIsExploreAds(false); - }, - ).toViewOnly(context, onTap: () { - navigateWithName(context, AppRoutes.loginWithPassword, arguments: false); - }), - ), - ], - ).horPaddingMain(), - if (adVM.isExploreAdsTapped) ...[ - if (adVM.adsFiltersCounter == 0) ...[ - 16.height, - FiltersList( - showImages: true, - filterList: adVM.exploreAdsFilterOptions, - onFilterTapped: (index, selectedFilterId) { - adVM.applyFilterOnExploreAds(vehicleBrandId: selectedFilterId); - }, - needLeftPadding: false, - ).paddingOnly(left: 21), - ] - ] else ...[ - 16.height, - FiltersList( - filterList: adVM.myAdsFilterOptions, - onFilterTapped: (index, selectedFilterId) { - adVM.applyFilterOnMyAds(adPostStatusEnum: selectedFilterId.toAdPostEnum()); - }, - needLeftPadding: false, - ).paddingOnly(left: 21), - ], - ], - ); - }, + ), + ), + if (adVM.isLoadingMore) ...[ + const Center( + child: Padding( + padding: EdgeInsets.all(8.0), + child: CircularProgressIndicator(), + )) + ] + ], ), - 16.height, - Expanded( - child: RefreshIndicator( - onRefresh: () async { - if (adVM.isExploreAdsTapped) { - int vehicleBrandId = adVM.exploreAdsFilterOptions.firstWhere((element) => element.isSelected).id; - adVM.applyFilterOnExploreAds(vehicleBrandId: vehicleBrandId); - } else { - AdPostStatus adPostStatusEnum = adVM.myAdsFilterOptions.firstWhere((element) => element.isSelected).id.toAdPostEnum(); - adVM.applyFilterOnMyAds(adPostStatusEnum: adPostStatusEnum); - } - }, - child: adVM.state == ViewState.busy - ? const AdsListShimmer(itemCount: 5) - : AdsListWidget( - isAdsFragment: true, - isDraftAds: false, - shouldShowAdStatus: !adVM.isExploreAdsTapped, - adsList: getAdsList(adVM), - hasMoreData: adVM.isExploreAdsTapped ? adVM.hasMoreDataForExploreAds : adVM.hasMoreDataForMyAds, - onFetchMoreAds: () async { - AdPostStatus adPostStatusEnum = adVM.myAdsFilterOptions.firstWhere((element) => element.isSelected).id.toAdPostEnum(); - - if (adVM.isExploreAdsTapped) { - await adVM.fetchMoreExploreAds(); - } else { - await adVM.fetchMoreMyAds(adsStatus: adPostStatusEnum); - } - }, - ), + ), + // Loading overlay when FAB is clicked and API is fetching + if (adVM.isLoadingAdsDuration) + Container( + width: double.infinity, + height: double.infinity, + color: Colors.black.withOpacity(0.3), + child: const Center( + child: CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation(MyColors.darkPrimaryColor), + ), ), ), - if (adVM.isLoadingMore) ...[ - const Center( - child: Padding( - padding: EdgeInsets.all(8.0), - child: CircularProgressIndicator(), - )) - ] - ], - ), + ], ), floatingActionButton: FloatingActionButton( onPressed: () async { - adVM.getVehicleAdsDuration(); + // ✅ Await API calls with loading state + await adVM.getVehicleAdsDuration(); + + // ✅ Ensure vehicle types are loaded + if (adVM.vehicleTypes.isEmpty) { + await adVM.getVehicleTypes(); + } + if (AppState().userType != UserType.providerDealer) { if (adVM.vehicleAdDurationId.selectedId == -1 && adVM.vehicleAdsDurations.isNotEmpty) { SelectionModel selection = SelectionModel( diff --git a/lib/views/requests/widget/request_item.dart b/lib/views/requests/widget/request_item.dart index e3f15e1..540d1c8 100644 --- a/lib/views/requests/widget/request_item.dart +++ b/lib/views/requests/widget/request_item.dart @@ -86,9 +86,9 @@ class RequestItem extends StatelessWidget { ), ], 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) ...[ - 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), ], ], ) diff --git a/lib/views/shipping_management/shipping_management_view.dart b/lib/views/shipping_management/shipping_management_view.dart index 001017b..f0a803b 100644 --- a/lib/views/shipping_management/shipping_management_view.dart +++ b/lib/views/shipping_management/shipping_management_view.dart @@ -121,6 +121,17 @@ class _ShippingManagementViewState extends State { }, ), 12.height, + // ✅ Show numberOfShipping field only when updating from pending to initiated (first update) + if (shippingRequestStatusEnum == ShippingRequestStatusEnum.pending) ...[ + TxtField( + value: shippingManagementVM.numberOfShipping, + errorValue: "", + keyboardType: TextInputType.text, + hint: LocaleKeys.numberOfShipping.tr(), + onChanged: (v) => shippingManagementVM.updateNumberOfShipping(v), + ), + 12.height, + ], TxtField( maxLines: 4, value: shippingManagementVM.requestStatusComments, @@ -202,6 +213,17 @@ class _ShippingManagementViewState extends State { }, ), 12.height, + // ✅ Show numberOfShipping field only when updating from pending to preparingToCollect (first update) + if (selfPickupRequestStatusEnum == SelfPickupRequestStatusEnum.pending) ...[ + TxtField( + value: shippingManagementVM.numberOfShipping, + errorValue: "", + keyboardType: TextInputType.text, + hint: LocaleKeys.numberOfShipping.tr(), + onChanged: (v) => shippingManagementVM.updateNumberOfShipping(v), + ), + 12.height, + ], TxtField( maxLines: 4, value: shippingManagementVM.requestStatusComments, @@ -352,6 +374,10 @@ class _ShippingManagementViewState extends State { if (shippingRequest.vehicleType != null && shippingRequest.vehicleType!.isNotEmpty) ...[ ("${LocaleKeys.itemType.tr()}: " "${shippingRequest.vehicleType}" ?? "").toText(color: MyColors.lightTextColor, fontSize: 12, fontWeight: MyFonts.Medium), ], + // ✅ Display numberOfShipping when not null + if (shippingRequest.numberOfShipping != null && shippingRequest.numberOfShipping!.isNotEmpty) ...[ + ("${LocaleKeys.numberOfShipping.tr()}: ${shippingRequest.numberOfShipping}").toText(color: MyColors.lightTextColor, fontSize: 12, fontWeight: MyFonts.Medium), + ], if (shippingRequest.comment != null && shippingRequest.comment!.isNotEmpty) ...[ ("${LocaleKeys.comment.tr()}: ${shippingRequest.comment ?? ""}").toText(color: MyColors.lightTextColor, fontSize: 12, fontWeight: MyFonts.Medium), ], diff --git a/lib/views/splash/splash_page.dart b/lib/views/splash/splash_page.dart index 3208edf..6f83629 100644 --- a/lib/views/splash/splash_page.dart +++ b/lib/views/splash/splash_page.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:mc_common_app/classes/consts.dart'; import 'package:mc_common_app/config/routes.dart'; import 'package:mc_common_app/utils/navigator.dart'; @@ -6,6 +7,8 @@ import 'package:mc_common_app/utils/utils.dart'; import 'package:mc_common_app/view_models/ad_view_model.dart'; import 'package:mc_common_app/view_models/chat_view_model.dart'; import 'package:provider/provider.dart'; +import 'package:audioplayers/audioplayers.dart'; +import 'dart:typed_data'; class SplashPage extends StatefulWidget { const SplashPage({super.key}); @@ -16,14 +19,29 @@ class SplashPage extends StatefulWidget { class SplashPageState extends State with WidgetsBindingObserver { bool showFlashingLights = false; + final AudioPlayer _audioPlayer = AudioPlayer(); @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); + _playEngineSound(); startSplashTimer(); } + Future _playEngineSound() async { + try { + // Load audio from package assets using rootBundle (same pattern as Image.asset) + // MyAssets.engineStartSound = "packages/mc_common_app/assets/tones/engine_start_tone.mp3" + final ByteData data = await rootBundle.load(MyAssets.engineStartSound); + final Uint8List bytes = data.buffer.asUint8List(); + await _audioPlayer.play(BytesSource(bytes)); + } catch (e) { + // Ignore errors if sound fails to play + debugPrint('Error playing engine sound: $e'); + } + } + void startSplashTimer() { // After 3 seconds, show the flashing lights gif Future.delayed(const Duration(seconds: 3), () { @@ -45,6 +63,8 @@ class SplashPageState extends State with WidgetsBindingObserver { @override void dispose() { WidgetsBinding.instance.removeObserver(this); + _audioPlayer.stop(); + _audioPlayer.dispose(); super.dispose(); } @@ -64,8 +84,12 @@ class SplashPageState extends State with WidgetsBindingObserver { // }); // } - navigateToNextDeciderPage() { - navigateReplaceWithName(context, AppRoutes.registerSelection); + navigateToNextDeciderPage() async { + // Stop the audio before navigating + await _audioPlayer.stop(); + if (mounted) { + navigateReplaceWithName(context, AppRoutes.registerSelection); + } } @override diff --git a/pubspec.lock b/pubspec.lock index 516fca5..3d5446b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -25,6 +25,62 @@ packages: url: "https://pub.dev" source: hosted version: "2.11.0" + audioplayers: + dependency: "direct main" + description: + name: audioplayers + sha256: f16640453cc47487b7de72a2b28d37c7df1ac97999849f4a46d92b1d2b0f093d + url: "https://pub.dev" + source: hosted + version: "6.7.1" + audioplayers_android: + dependency: transitive + description: + name: audioplayers_android + sha256: "60a6728277228413a85755bd3ffd6fab98f6555608923813ce383b190a360605" + url: "https://pub.dev" + source: hosted + version: "5.2.1" + audioplayers_darwin: + dependency: transitive + description: + name: audioplayers_darwin + sha256: c994b3bb3a921e4904ac40e013fbc94488e824fd7c1de6326f549943b0b44a91 + url: "https://pub.dev" + source: hosted + version: "6.4.0" + audioplayers_linux: + dependency: transitive + description: + name: audioplayers_linux + sha256: f75bce1ce864170ef5e6a2c6a61cd3339e1a17ce11e99a25bae4474ea491d001 + url: "https://pub.dev" + source: hosted + version: "4.2.1" + audioplayers_platform_interface: + dependency: transitive + description: + name: audioplayers_platform_interface + sha256: "0e2f6a919ab56d0fec272e801abc07b26ae7f31980f912f24af4748763e5a656" + url: "https://pub.dev" + source: hosted + version: "7.1.1" + audioplayers_web: + dependency: transitive + description: + name: audioplayers_web + sha256: "24a6f258062bd7da8cb2157e83fccb9816a08dd306cbaaa24f9813d071470545" + url: "https://pub.dev" + source: hosted + version: "5.2.1" + audioplayers_windows: + dependency: transitive + description: + name: audioplayers_windows + sha256: "95f875a96c88c3dbbcb608d4f8288e300b0113d256a81d0b3197fcc18f0dc91a" + url: "https://pub.dev" + source: hosted + version: "4.3.1" auto_size_text: dependency: "direct main" description: @@ -1484,4 +1540,4 @@ packages: version: "6.5.0" sdks: dart: ">=3.8.0-0 <4.0.0" - flutter: ">=3.24.0" + flutter: ">=3.35.0" diff --git a/pubspec.yaml b/pubspec.yaml index d78b051..3946f8a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -43,6 +43,7 @@ dependencies: change_app_package_name: ^1.3.0 share_plus: any toastification: ^2.3.0 + audioplayers: ^6.0.0 geolocator_android: 4.5.5 @@ -78,6 +79,7 @@ flutter: assets: - assets/ - assets/langs/ + - assets/tones/ - assets/icons/ - assets/icons/payments/ - assets/images/