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 {
final countryId = AppState().getUser.data!.userInfo!.countryId; isLoadingAdsDuration = true;
vehicleAdsDurations = await adsRepo.getAdsDuration(countryId: countryId);
notifyListeners(); notifyListeners();
try {
final countryId = AppState().getUser.data!.userInfo!.countryId;
vehicleAdsDurations = await adsRepo.getAdsDuration(countryId: countryId);
isLoadingAdsDuration = false;
notifyListeners();
} catch (e) {
isLoadingAdsDuration = false;
notifyListeners();
rethrow;
}
} }
Future<void> getVehicleAdsSpecialServices({required int cityId, required int countryId, required int specialServiceType}) async { 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,83 +559,112 @@ class ChatVM extends BaseVM {
}) async { }) async {
if (chatMessageType == ChatMessageTypeEnum.freeText && message.isEmpty) return false; if (chatMessageType == ChatMessageTypeEnum.freeText && message.isEmpty) return false;
// Ensure connection is established // Prevent duplicate sends
if (hubConnection == null || hubConnection!.state != HubConnectionState.connected) { if (isSendingMessage) {
logger.i("Hub Not Connected!! Attempting to connect..."); logger.w("Message already being sent, ignoring duplicate tap");
await buildHubConnection(context: context); return false;
}
// Wait a bit for connection to establish isSendingMessage = true;
await Future.delayed(const Duration(milliseconds: 500)); notifyListeners();
// Check again after attempt try {
// Ensure connection is established
if (hubConnection == null || hubConnection!.state != HubConnectionState.connected) { if (hubConnection == null || hubConnection!.state != HubConnectionState.connected) {
Utils.showToast("Failed to connect. Please try again."); logger.i("Hub Not Connected!! Attempting to connect...");
return false; await buildHubConnection(context: context);
}
} // Wait a bit for connection to establish
await Future.delayed(const Duration(milliseconds: 500));
if (hubConnection!.state == HubConnectionState.connected) {
final userId = AppState().getUser.data!.userInfo!.userId.toString(); // Check again after attempt
final name = AppState().getUser.data!.userInfo!.firstName.toString(); if (hubConnection == null || hubConnection!.state != HubConnectionState.connected) {
List<Map<String, dynamic>> requestImagesMap = []; Utils.showToast("Failed to connect. Please try again.");
List<MessageImageModel> messageImages = getMessagesImageList(); isSendingMessage = false;
if (messageImages.isNotEmpty && chatMessageType == ChatMessageTypeEnum.image) { notifyListeners();
for (var element in messageImages) { return false;
requestImagesMap.add(element.toJson());
} }
} }
final obj = <String, dynamic>{
"ReceiverUserID": receiverId,
"SenderUserID": userId,
"MessageType": chatMessageType.getIdFromChatMessageTypeEnum(),
"ChatText": chatMessageType == ChatMessageTypeEnum.image ? "${messageImages.length} Image(s)" : message,
"RequestID": requestId,
"ReqOfferID": latestOfferId,
};
if (chatMessageType == ChatMessageTypeEnum.image) {
obj.addAll({"ReqOfferImages": requestImagesMap});
}
logger.i(obj);
try {
await hubConnection!.invoke(
SignalrConsts.sendMessageRequestOffer,
args: <Object>[obj],
);
ChatMessageModel chatMessageModel = ChatMessageModel( if (hubConnection!.state == HubConnectionState.connected) {
messageType: chatMessageType.getIdFromChatMessageTypeEnum(), final userId = AppState().getUser.data!.userInfo!.userId.toString();
chatText: message, final name = AppState().getUser.data!.userInfo!.firstName.toString();
isMyMessage: true, List<Map<String, dynamic>> requestImagesMap = [];
requestID: requestId, List<MessageImageModel> messageImages = getMessagesImageList();
senderName: name, if (messageImages.isNotEmpty && chatMessageType == ChatMessageTypeEnum.image) {
senderUserID: userId, for (var element in messageImages) {
chatMessageTypeEnum: chatMessageType, requestImagesMap.add(element.toJson());
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 { }
int providerIndex = context.read<RequestsVM>().myFilteredRequests.indexWhere((element) => (element.customerUserID == receiverId && element.id == requestId)); final obj = <String, dynamic>{
log("providerIndex2:$providerIndex"); "ReceiverUserID": receiverId,
if (providerIndex != -1) { "SenderUserID": userId,
context.read<RequestsVM>().addChatMessagesInRequestsModel(msg: chatMessageModel, index: providerIndex); "MessageType": chatMessageType.getIdFromChatMessageTypeEnum(),
"ChatText": chatMessageType == ChatMessageTypeEnum.image ? "${messageImages.length} Image(s)" : message,
"RequestID": requestId,
"ReqOfferID": latestOfferId,
};
if (chatMessageType == ChatMessageTypeEnum.image) {
obj.addAll({"ReqOfferImages": requestImagesMap});
}
logger.i(obj);
try {
await hubConnection!.invoke(
SignalrConsts.sendMessageRequestOffer,
args: <Object>[obj],
);
ChatMessageModel chatMessageModel = ChatMessageModel(
messageType: chatMessageType.getIdFromChatMessageTypeEnum(),
chatText: message,
isMyMessage: true,
requestID: requestId,
senderName: name,
senderUserID: userId,
chatMessageTypeEnum: chatMessageType,
receiverUserID: receiverId,
messageImages: messageImages,
);
if (AppState().currentAppType == AppType.customer) {
int providerIndex = serviceProviderOffersList.indexWhere((element) => element.providerUserId == receiverId);
if (providerIndex != -1) {
serviceProviderOffersList[providerIndex].chatMessages!.add(chatMessageModel);
}
} else {
// Use stored RequestsVM instead of context.read
if (_requestsVM != null) {
int providerIndex = _requestsVM!.myFilteredRequests.indexWhere((element) => (element.customerUserID == receiverId && element.id == requestId));
log("providerIndex2:$providerIndex");
if (providerIndex != -1) {
_requestsVM!.addChatMessagesInRequestsModel(msg: chatMessageModel, index: providerIndex);
}
}
} }
isSendingMessage = false;
notifyListeners();
return true;
} catch (e) {
logger.e("error in invoking SendMessage: ${e.toString()}");
Utils.showToast("Failed to send message");
isSendingMessage = false;
notifyListeners();
return false;
} }
return true;
} catch (e) {
logger.e("error in invoking SendMessage: ${e.toString()}");
Utils.showToast("Failed to send message");
return false;
} }
isSendingMessage = false;
notifyListeners();
return false;
} catch (e) {
logger.e("Unexpected error in onMessageSendForRequest: ${e.toString()}");
isSendingMessage = false;
notifyListeners();
return false;
} }
return false;
} }
List<ServiceProvidersOffers> serviceProviderOffersList = []; 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,63 +904,89 @@ class ChatVM extends BaseVM {
}) async { }) async {
if (message.isEmpty) return false; if (message.isEmpty) return false;
// Ensure connection is established // Prevent duplicate sends
if (hubConnection == null || hubConnection!.state != HubConnectionState.connected) { if (isSendingMessage) {
logger.i("Hub Not Connected!! Attempting to connect..."); logger.w("Message already being sent, ignoring duplicate tap");
await buildHubConnection(context: context); return false;
}
// Wait a bit for connection to establish isSendingMessage = true;
await Future.delayed(const Duration(milliseconds: 500)); notifyListeners();
// Check again after attempt try {
// Ensure connection is established
if (hubConnection == null || hubConnection!.state != HubConnectionState.connected) { if (hubConnection == null || hubConnection!.state != HubConnectionState.connected) {
Utils.showToast("Failed to connect. Please try again."); logger.i("Hub Not Connected!! Attempting to connect...");
return false; await buildHubConnection(context: context);
// Wait a bit for connection to establish
await Future.delayed(const Duration(milliseconds: 500));
// Check again after attempt
if (hubConnection == null || hubConnection!.state != HubConnectionState.connected) {
Utils.showToast("Failed to connect. Please try again.");
isSendingMessage = false;
notifyListeners();
return false;
}
} }
}
if (hubConnection!.state == HubConnectionState.connected) {
final userId = AppState().getUser.data!.userInfo!.userId.toString();
final name = AppState().getUser.data!.userInfo!.firstName.toString();
final obj = <String, dynamic>{
"ReceiverUserID": receiverId,
// "SenderUserID": userId,
"MessageType": chatMessageType.getIdFromChatMessageTypeEnum(),
"ChatText": message,
"AdsID": adId,
};
logger.i("$obj");
try {
await hubConnection!.invoke(
SignalrConsts.sendMessageAds,
args: <Object>[obj],
);
ChatMessageModel chatMessageModel = ChatMessageModel( if (hubConnection!.state == HubConnectionState.connected) {
messageType: chatMessageType.getIdFromChatMessageTypeEnum(), final userId = AppState().getUser.data!.userInfo!.userId.toString();
chatText: message, final name = AppState().getUser.data!.userInfo!.firstName.toString();
isMyMessage: true, final obj = <String, dynamic>{
senderName: name, "ReceiverUserID": receiverId,
senderUserID: userId, // "SenderUserID": userId,
chatMessageTypeEnum: ChatMessageTypeEnum.freeText, "MessageType": chatMessageType.getIdFromChatMessageTypeEnum(),
receiverUserID: receiverId, "ChatText": message,
); "AdsID": adId,
};
currentMessagesForAds.add(chatMessageModel); logger.i("$obj");
notifyListeners(); try {
await hubConnection!.invoke(
if (AppState().currentAppType == AppType.customer) { SignalrConsts.sendMessageAds,
} else {} args: <Object>[obj],
return true; );
} catch (e) {
logger.e("error in invoking SendMessage: ${e.toString()}"); ChatMessageModel chatMessageModel = ChatMessageModel(
Utils.showToast("Failed to send message"); messageType: chatMessageType.getIdFromChatMessageTypeEnum(),
return false; 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 ================== // ========================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,59 +1052,85 @@ class ChatVM extends BaseVM {
}) async { }) async {
if (message.isEmpty) return false; if (message.isEmpty) return false;
// Ensure connection is established // Prevent duplicate sends
if (hubConnection == null || hubConnection!.state != HubConnectionState.connected) { if (isSendingMessage) {
logger.i("Hub Not Connected!! Attempting to connect..."); logger.w("Message already being sent, ignoring duplicate tap");
await buildHubConnection(context: context); return false;
}
// Wait a bit for connection to establish isSendingMessage = true;
await Future.delayed(const Duration(milliseconds: 500)); notifyListeners();
// Check again after attempt try {
// Ensure connection is established
if (hubConnection == null || hubConnection!.state != HubConnectionState.connected) { if (hubConnection == null || hubConnection!.state != HubConnectionState.connected) {
Utils.showToast("Failed to connect. Please try again."); logger.i("Hub Not Connected!! Attempting to connect...");
return false; await buildHubConnection(context: context);
// Wait a bit for connection to establish
await Future.delayed(const Duration(milliseconds: 500));
// Check again after attempt
if (hubConnection == null || hubConnection!.state != HubConnectionState.connected) {
Utils.showToast("Failed to connect. Please try again.");
isSendingMessage = false;
notifyListeners();
return false;
}
} }
}
if (hubConnection!.state == HubConnectionState.connected) {
final userId = AppState().getUser.data!.userInfo!.userId.toString();
final name = AppState().getUser.data!.userInfo!.firstName.toString();
final obj = <String, dynamic>{
"ReceiverUserID": receiverId,
"MessageType": chatMessageType.getIdFromChatMessageTypeEnum(),
"ChatText": message,
};
logger.i("$obj");
try { if (hubConnection!.state == HubConnectionState.connected) {
await hubConnection!.invoke( final userId = AppState().getUser.data!.userInfo!.userId.toString();
SignalrConsts.sendMessageGeneral, final name = AppState().getUser.data!.userInfo!.firstName.toString();
args: <Object>[obj], final obj = <String, dynamic>{
); "ReceiverUserID": receiverId,
"MessageType": chatMessageType.getIdFromChatMessageTypeEnum(),
ChatMessageModel chatMessageModel = ChatMessageModel( "ChatText": message,
messageType: chatMessageType.getIdFromChatMessageTypeEnum(), };
chatText: message, logger.i("$obj");
isMyMessage: true,
senderName: name, try {
senderUserID: userId, await hubConnection!.invoke(
chatMessageTypeEnum: ChatMessageTypeEnum.freeText, SignalrConsts.sendMessageGeneral,
receiverUserID: receiverId, args: <Object>[obj],
); );
currentMessagesForGeneralChat.add(chatMessageModel); ChatMessageModel chatMessageModel = ChatMessageModel(
notifyListeners(); messageType: chatMessageType.getIdFromChatMessageTypeEnum(),
chatText: message,
if (AppState().currentAppType == AppType.customer) { isMyMessage: true,
} else {} senderName: name,
return true; senderUserID: userId,
} catch (e) { chatMessageTypeEnum: ChatMessageTypeEnum.freeText,
logger.e("error in invoking SendMessage: ${e.toString()}"); receiverUserID: receiverId,
Utils.showToast("Failed to send message"); );
return false;
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;
} }
} }

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

@ -62,124 +62,147 @@ 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(
width: double.infinity, children: [
height: double.infinity, SizedBox(
child: Column( width: double.infinity,
children: [ height: double.infinity,
16.height, child: Column(
Consumer( children: [
builder: (BuildContext context, AdVM adVM, Widget? child) { 16.height,
return Column( Consumer(
children: [ builder: (BuildContext context, AdVM adVM, Widget? child) {
Row( return Column(
children: [ children: [
Expanded( Row(
child: ShowFillButton( children: [
isFilled: adVM.isExploreAdsTapped, Expanded(
maxHeight: 55, child: ShowFillButton(
fontSize: 15, isFilled: adVM.isExploreAdsTapped,
title: LocaleKeys.exploreAds.tr(), maxHeight: 55,
txtColor: adVM.isExploreAdsTapped ? MyColors.white : MyColors.darkTextColor, fontSize: 15,
onPressed: () { title: LocaleKeys.exploreAds.tr(),
log("accessToken: ${AppState().getUser.data!.accessToken}"); txtColor: adVM.isExploreAdsTapped ? MyColors.white : MyColors.darkTextColor,
log("AppState().getUser.data!.userInfo!.userId;: ${AppState().getUser.data!.userInfo!.userId}"); onPressed: () {
log("accessToken: ${AppState().getUser.data!.accessToken}");
log("AppState().getUser.data!.userInfo!.userId;: ${AppState().getUser.data!.userInfo!.userId}");
if (adVM.myAds.isEmpty) { if (adVM.myAds.isEmpty) {
adVM.getMyAds(); 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( if (adVM.isLoadingMore) ...[
child: ShowFillButton( const Center(
isFilled: !adVM.isExploreAdsTapped, child: Padding(
txtColor: !adVM.isExploreAdsTapped ? MyColors.white : MyColors.darkTextColor, padding: EdgeInsets.all(8.0),
maxHeight: 55, child: CircularProgressIndicator(),
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( // Loading overlay when FAB is clicked and API is fetching
child: RefreshIndicator( if (adVM.isLoadingAdsDuration)
onRefresh: () async { Container(
if (adVM.isExploreAdsTapped) { width: double.infinity,
int vehicleBrandId = adVM.exploreAdsFilterOptions.firstWhere((element) => element.isSelected).id; height: double.infinity,
adVM.applyFilterOnExploreAds(vehicleBrandId: vehicleBrandId); color: Colors.black.withOpacity(0.3),
} else { child: const Center(
AdPostStatus adPostStatusEnum = adVM.myAdsFilterOptions.firstWhere((element) => element.isSelected).id.toAdPostEnum(); child: CircularProgressIndicator(
adVM.applyFilterOnMyAds(adPostStatusEnum: adPostStatusEnum); valueColor: AlwaysStoppedAnimation<Color>(MyColors.darkPrimaryColor),
} ),
},
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);
}
},
),
), ),
), ),
if (adVM.isLoadingMore) ...[ ],
const Center(
child: Padding(
padding: EdgeInsets.all(8.0),
child: CircularProgressIndicator(),
))
]
],
),
), ),
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,8 +84,12 @@ class SplashPageState extends State<SplashPage> with WidgetsBindingObserver {
// }); // });
// } // }
navigateToNextDeciderPage() { navigateToNextDeciderPage() async {
navigateReplaceWithName(context, AppRoutes.registerSelection); // Stop the audio before navigating
await _audioPlayer.stop();
if (mounted) {
navigateReplaceWithName(context, AppRoutes.registerSelection);
}
} }
@override @override

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