Completed the Enhancement CR

master
Faiz Hashmi 3 months ago
parent ff10b66b14
commit 3146a9433b

@ -420,8 +420,12 @@
"accidentFree": "خالية من الحوادث",
"brandNew": "جديدة",
"used": "مستعملة",
"all": "الكل",
"newSparePart": "قطع غيار جديدة",
"usedSparePart": "قطع غيار مستعملة",
"hasYourCarHadAccident": "هل تعرضت سيارتك لأي حادث؟",
"odometerReading": "قراءة عداد المسافات (كم)",
"canMileageIncrease": "هل يمكن زيادة المسافة المقطوعة عند الفحص؟",
"currentlyInUse": "قيد الاستخدام حاليًا",
"demand": "الطلب",
"adDurationExpired": "انتهت مدة عرض الإعلان الخاص بك",

@ -421,8 +421,12 @@
"accidentFree": "Accident Free",
"brandNew": "Brand New",
"used": "Used",
"all": "All",
"newSparePart": "New Spare Part",
"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",

@ -0,0 +1,3 @@
description: This file stores settings for Dart & Flutter DevTools.
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
extensions:

@ -138,7 +138,7 @@ class ApiClientImp implements ApiClient {
logger.i(url);
logger.d("------Payload------");
logger.i(jsonDecode(requestBody));
logger.d("------Response------");
// logger.d("------Response------");
logger.i(jsonDecode(response.body));
}
if (response.statusCode >= 200 && response.statusCode < 500) {

@ -12,20 +12,21 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:mc_common_app/generated/locale_keys.g.dart';
extension EmailValidator on String {
Widget toText({Color? color,
bool isBold = false,
double? fontSize,
bool isUnderLine = false,
bool isItalic = false,
TextDecoration? textDecoration,
double letterSpacing = 0,
TextAlign? textAlign,
FontWeight? fontWeight,
double? height,
Color? decorationColor,
TextOverflow? overflow,
bool isUpperCase = false,
int? maxLines}) {
Widget toText(
{Color? color,
bool isBold = false,
double? fontSize,
bool isUnderLine = false,
bool isItalic = false,
TextDecoration? textDecoration,
double letterSpacing = 0,
TextAlign? textAlign,
FontWeight? fontWeight,
double? height,
Color? decorationColor,
TextOverflow? overflow,
bool isUpperCase = false,
int? maxLines}) {
bool isArabic = AppState().postParamsObject!.languageID == 1; // arabic -> 1 , English -> 2
return AutoSizeText(
isUpperCase ? toUpperCase() : this,
@ -443,6 +444,8 @@ extension RequestTypeTypEnum on int {
return RequestsTypeEnum.specialCarRequest;
} else if (this == 2) {
return RequestsTypeEnum.serviceRequest;
} else if (this == 3) {
return RequestsTypeEnum.other;
}
return RequestsTypeEnum.specialCarRequest;
}
@ -457,6 +460,9 @@ extension RequestTypeStatusToInt on RequestsTypeEnum {
case RequestsTypeEnum.serviceRequest:
return 2;
case RequestsTypeEnum.other:
return 3;
default:
return 0;
}

@ -270,6 +270,9 @@ class CodegenLoader extends AssetLoader{
"sendOTP": "إرسال رمز OTP",
"noUserAddedYet": "لم يتم إضافة مستخدم بعد",
"assignDealerUser": "تعيين مستخدم تاجر",
"confirmAssignDealerUser": "هل أنت متأكد من رغبتك في تعيين مستخدم التاجر هذا للفرع؟",
"removeDealerUser": "إزالة مستخدم تاجر",
"confirmRemoveDealerUser": "هل أنت متأكد من رغبتك في إزالة مستخدم التاجر هذا من الفرع؟",
"createNewUser": "إنشاء مستخدم جديد",
"dealerUser": "مستخدم تاجر",
"setorEditBranchSchedule": "تعيين أو تحرير جدول الفروع",
@ -433,6 +436,9 @@ class CodegenLoader extends AssetLoader{
"accidentFree": "خالية من الحوادث",
"brandNew": "جديدة",
"used": "مستعملة",
"all": "الكل",
"newSparePart": "قطع غيار جديدة",
"usedSparePart": "قطع غيار مستعملة",
"hasYourCarHadAccident": "هل تعرضت سيارتك لأي حادث؟",
"odometerReading": "قراءة عداد المسافات (كم)",
"currentlyInUse": "قيد الاستخدام حاليًا",
@ -1168,6 +1174,9 @@ static const Map<String,dynamic> _en_US = {
"sendOTP": "Send OTP",
"noUserAddedYet": "No User Added Yet",
"assignDealerUser": "Assign Dealer User",
"confirmAssignDealerUser": "Are you sure you want to assign this dealer user to the branch?",
"removeDealerUser": "Remove Dealer User",
"confirmRemoveDealerUser": "Are you sure you want to remove this dealer user from the branch?",
"createNewUser": "Create New User",
"dealerUser": "Dealer User",
"setorEditBranchSchedule": "Set or Edit Branch Schedule",
@ -1332,6 +1341,9 @@ static const Map<String,dynamic> _en_US = {
"accidentFree": "Accident Free",
"brandNew": "Brand New",
"used": "Used",
"all": "All",
"newSparePart": "New Spare Part",
"usedSparePart": "Used Spare Part",
"hasYourCarHadAccident": "Has your car had any accident?",
"odometerReading": "Odometer Reading (Km)",
"currentlyInUse": "Currently in Use",

@ -2,7 +2,7 @@
// ignore_for_file: constant_identifier_names
abstract class LocaleKeys {
abstract class LocaleKeys {
static const firstTimeLogIn = 'firstTimeLogIn';
static const signUp = 'signUp';
static const changeMobile = 'changeMobile';
@ -49,7 +49,7 @@ abstract class LocaleKeys {
static const emailChangedSuccessfully = 'emailChangedSuccessfully';
static const passwordIsUpdated = 'passwordIsUpdated';
static const passwordShouldContains = 'passwordShouldContains';
static const successfullyRegistered = ' successfullyRegistered';
static const successfullyRegistered = ' successfullyRegistered';
static const pleaseEnterSamePassword = 'pleaseEnterSamePassword';
static const firstNameMandatory = 'firstNameMandatory';
static const surnameNameMandatory = 'surnameNameMandatory';
@ -401,6 +401,9 @@ abstract class LocaleKeys {
static const accidentFree = 'accidentFree';
static const brandNew = 'brandNew';
static const used = 'used';
static const all = 'all';
static const newSparePart = 'newSparePart';
static const usedSparePart = 'usedSparePart';
static const hasYourCarHadAccident = 'hasYourCarHadAccident';
static const odometerReading = 'odometerReading';
static const currentlyInUse = 'currentlyInUse';

@ -1,12 +1,14 @@
class FilterListModel {
String title;
String? titleN;
String titleN;
String description;
String descriptionN;
String iconUrl;
int id;
bool isSelected;
bool isCustom;
FilterListModel(
{required this.id, required this.isSelected, this.iconUrl = "", required this.title, this.titleN});
FilterListModel({required this.id, required this.isSelected, this.isCustom = false, this.iconUrl = "", required this.title, this.titleN = "", this.description = "", this.descriptionN = "",});
}
class SelectionModel {

@ -19,7 +19,7 @@ abstract class RequestRepo {
required String brand,
required String model,
required String year,
required bool isNew,
required int conditionType,
required int countryID,
required int cityID,
required String price,
@ -88,7 +88,7 @@ class RequestRepoImp implements RequestRepo {
required String brand,
required String model,
required String year,
required bool isNew,
required int conditionType,
required int countryID,
required int cityID,
required String price,
@ -103,7 +103,7 @@ class RequestRepoImp implements RequestRepo {
"brand": brand,
"model": model,
"year": year,
"isNew": isNew,
"conditionType": conditionType,
"countryID": countryID,
"cityID": cityID,
"price": price,
@ -181,9 +181,13 @@ class RequestRepoImp implements RequestRepo {
"model": vehicleModel.toString(),
"year": vehicleYearId.toString(),
"cityID": cityId.toString(),
"isNew": conditionId == 0 ? "" : conditionId == 1,
};
// Add conditionType if provided (1=New, 2=Used, 3=All)
if (conditionId != null && conditionId != 0) {
paramsForGetRequests["conditionType"] = conditionId.toString();
}
if (appState.currentAppType == AppType.customer) {
if (!appState.getIsViewOnly) {
paramsForGetRequests.addEntries([MapEntry("customerID", appState.getUser.data!.userInfo!.customerId ?? 0)]);

@ -192,6 +192,7 @@ enum AppointmentPaymentStatusEnum {
enum RequestsTypeEnum {
specialCarRequest,
serviceRequest,
other,
}
enum ChatMessageTypeEnum {

@ -37,12 +37,11 @@ class Utils {
type: ToastificationType.info,
style: ToastificationStyle.minimal,
title: Text(
message,
maxLines: 2,
maxLines: 5,
overflow: TextOverflow.ellipsis,
),
autoCloseDuration: const Duration(seconds: 2),
autoCloseDuration: const Duration(seconds: 3),
alignment: Alignment.bottomCenter,
showProgressBar: false,
);
@ -445,27 +444,27 @@ class Utils {
case BranchStatusEnum.approvedOrActive:
return LocaleKeys.active.tr();
}
// static String getChipStatusTextByBranchStatus(BranchStatusEnum branchStatusEnum) {
// switch (branchStatusEnum) {
// case BranchStatusEnum.pending:
// return "Pending Approval";
//
// case BranchStatusEnum.review:
// return "In Review";
//
// case BranchStatusEnum.rejected:
// return "Rejected";
//
// case BranchStatusEnum.deactivated:
// return "Deactivated";
//
// case BranchStatusEnum.blocked:
// return "Blocked";
//
// case BranchStatusEnum.approvedOrActive:
// return "Active";
// }
}
// static String getChipStatusTextByBranchStatus(BranchStatusEnum branchStatusEnum) {
// switch (branchStatusEnum) {
// case BranchStatusEnum.pending:
// return "Pending Approval";
//
// case BranchStatusEnum.review:
// return "In Review";
//
// case BranchStatusEnum.rejected:
// return "Rejected";
//
// case BranchStatusEnum.deactivated:
// return "Deactivated";
//
// case BranchStatusEnum.blocked:
// return "Blocked";
//
// case BranchStatusEnum.approvedOrActive:
// return "Active";
// }
}
static statusContainerChip({
required String text,

@ -771,7 +771,7 @@ class AdVM extends BaseVM {
notifyListeners();
}
int isInUsed = 0;
int isInUsed = 0; // 0 = No, 1 = Yes
void updateIsInUsed(int value) {
isInUsed = value;

@ -52,8 +52,7 @@ class AppointmentsVM extends BaseVM {
final BranchRepo branchRepo;
final AppointmentRepo appointmentRepo;
AppointmentsVM(
{required this.commonServices, required this.appointmentRepo, required this.commonRepo, required this.branchRepo});
AppointmentsVM({required this.commonServices, required this.appointmentRepo, required this.commonRepo, required this.branchRepo});
bool isUpcomingEnabled = true;
bool isFetchingLists = false;
@ -80,8 +79,7 @@ class AppointmentsVM extends BaseVM {
List<ItemData> allSelectedItemsInAppointments = [];
bool ifItemAlreadySelected(int id) {
int indexFound = allSelectedItemsInAppointments.indexWhere((
element) => element.id == id);
int indexFound = allSelectedItemsInAppointments.indexWhere((element) => element.id == id);
log("already selected at: $indexFound");
if (indexFound != -1) {
return true;
@ -109,8 +107,7 @@ class AppointmentsVM extends BaseVM {
}
}
Future<void> onPayNowPressedForAppointment(
{required BuildContext context, required int appointmentID}) async {
Future<void> onPayNowPressedForAppointment({required BuildContext context, required int appointmentID}) async {
context.read<PaymentVM>().updateAppointmentIdsForPayment(
ids: [appointmentID]);
navigateWithName(context, AppRoutes.paymentMethodsView,
@ -168,8 +165,7 @@ class AppointmentsVM extends BaseVM {
}
}
Future<void> onConfirmAppointmentPressed(
{required BuildContext context, required appointmentId}) async {
Future<void> onConfirmAppointmentPressed({required BuildContext context, required appointmentId}) async {
context.read<PaymentVM>().updateAppointmentIdsForPayment(
ids: [appointmentId]);
navigateWithName(context, AppRoutes.paymentMethodsView,
@ -224,8 +220,7 @@ class AppointmentsVM extends BaseVM {
}
}
Future<void> onCancelAppointmentPressed(
{required BuildContext context, required AppointmentListModel appointmentListModel}) async {
Future<void> onCancelAppointmentPressed({required BuildContext context, required AppointmentListModel appointmentListModel}) async {
Utils.showLoading(context);
try {
GenericRespModel genericRespModel = await appointmentRepo
@ -378,20 +373,19 @@ class AppointmentsVM extends BaseVM {
for (int i = 0; i < myAppointmentsEnum.length; i++) {
appointmentsFilterOptions.add(FilterListModel(
title: myAppointmentsEnum[i].enumValueStr,
titleN: myAppointmentsEnum[i].enumValueStrN,
titleN: myAppointmentsEnum[i].enumValueStrN,
isSelected: false,
id: myAppointmentsEnum[i].enumValue));
}
appointmentsFilterOptions.insert(
0, FilterListModel(title: "All Appointments",titleN: "جميع الفروع", isSelected: true, id: 0));
0, FilterListModel(title: "All Appointments", titleN: "جميع الفروع", isSelected: true, id: 0));
notifyListeners();
}
bool isAppointmentLoading = false;
applyFilterOnAppointmentsVMForProviders(
{int? branchID, required AppointmentStatusEnum appointmentStatusEnum, bool isNeedCustomerFilter = false, bool shouldPopulateUpcoming = false}) async {
applyFilterOnAppointmentsVMForProviders({int? branchID, required AppointmentStatusEnum appointmentStatusEnum, bool isNeedCustomerFilter = false, bool shouldPopulateUpcoming = false}) async {
if (appointmentsFilterOptions.isEmpty) return;
for (var value in appointmentsFilterOptions) {
value.isSelected = false;
@ -430,8 +424,7 @@ class AppointmentsVM extends BaseVM {
notifyListeners();
}
applyFilterOnAppointmentsVMForCustomers(
{required AppointmentStatusEnum appointmentStatusEnum, bool shouldPopulateUpcoming = false}) async {
applyFilterOnAppointmentsVMForCustomers({required AppointmentStatusEnum appointmentStatusEnum, bool shouldPopulateUpcoming = false}) async {
log("appointmentsFilterOptions: ${appointmentsFilterOptions.isEmpty}");
if (appointmentsFilterOptions.isEmpty) return;
for (var value in appointmentsFilterOptions) {
@ -460,8 +453,7 @@ class AppointmentsVM extends BaseVM {
notifyListeners();
}
List<AppointmentListModel> findAppointmentsBasedOnCustomers(
List<AppointmentListModel> appointments) {
List<AppointmentListModel> findAppointmentsBasedOnCustomers(List<AppointmentListModel> appointments) {
Set<int> uniqueCustomerIDs = <int>{};
for (var item in appointments) {
uniqueCustomerIDs.add(item.customerID ?? 0);
@ -511,8 +503,7 @@ class AppointmentsVM extends BaseVM {
}
}
Future<bool> updateAppointmentStatus(
{required int appointmentId, required AppointmentStatusEnum appointmentStatusEnum, bool isNeedToRebuild = false}) async {
Future<bool> updateAppointmentStatus({required int appointmentId, required AppointmentStatusEnum appointmentStatusEnum, bool isNeedToRebuild = false}) async {
if (isNeedToRebuild) setState(ViewState.busy);
try {
GenericRespModel genericRespModel = await appointmentRepo
@ -599,8 +590,7 @@ class AppointmentsVM extends BaseVM {
.length;
}
updateSelectedAppointmentDate(
{required int dateIndex, required int scheduleIndex}) {
updateSelectedAppointmentDate({required int dateIndex, required int scheduleIndex}) {
for (var element in serviceAppointmentScheduleList[scheduleIndex]
.customTimeDateSlotList!) {
element.date!.isSelected = false;
@ -622,8 +612,7 @@ class AppointmentsVM extends BaseVM {
notifyListeners();
}
updateSelectedAppointmentSlotByDate(
{required int scheduleIndex, required int slotIndex}) {
updateSelectedAppointmentSlotByDate({required int scheduleIndex, required int slotIndex}) {
for (var element in serviceAppointmentScheduleList[scheduleIndex]
.customTimeDateSlotList!) {
for (var element in element.availableSlots!) {
@ -652,8 +641,7 @@ class AppointmentsVM extends BaseVM {
int selectedSubServicesCounter = 0;
void onItemUpdateOrSelected(int index, bool selected, int itemId) {
int serviceIndex = servicesInCurrentAppointment.indexWhere((
element) => element.serviceId == currentServiceSelection!.serviceId!);
int serviceIndex = servicesInCurrentAppointment.indexWhere((element) => element.serviceId == currentServiceSelection!.serviceId!);
serviceItemsFromApi[index].isUpdateOrSelected = selected;
serviceItemsFromApi[index].isHomeSelected = isHomeTapped;
@ -983,8 +971,7 @@ class AppointmentsVM extends BaseVM {
notifyListeners();
}
Future<void> onRescheduleAppointmentPressed(
{required BuildContext context, required AppointmentListModel appointmentListModel}) async {
Future<void> onRescheduleAppointmentPressed({required BuildContext context, required AppointmentListModel appointmentListModel}) async {
Utils.showLoading(context);
List<String> serviceItemIdsForHome = [];
@ -1024,8 +1011,7 @@ class AppointmentsVM extends BaseVM {
notifyListeners();
}
Future<void> onRescheduleAppointmentConfirmPressed(
{required BuildContext context, required int appointmentId, required int selectedSlotId}) async {
Future<void> onRescheduleAppointmentConfirmPressed({required BuildContext context, required int appointmentId, required int selectedSlotId}) async {
Utils.showLoading(context);
try {
GenericRespModel genericRespModel = await appointmentRepo
@ -1066,8 +1052,7 @@ class AppointmentsVM extends BaseVM {
}
}
Future buildComplaintBottomSheet(
{required int customerID, required int complainType, required String customerName, required BuildContext context}) {
Future buildComplaintBottomSheet({required int customerID, required int complainType, required String customerName, required BuildContext context}) {
return showModalBottomSheet(
context: context,
isScrollControlled: true,
@ -1129,8 +1114,7 @@ class AppointmentsVM extends BaseVM {
);
}
Future<bool> reportComplaintAgainstCustomer(
{required int customerID, required String comment, required int complaintType, required BuildContext context}) async {
Future<bool> reportComplaintAgainstCustomer({required int customerID, required String comment, required int complaintType, required BuildContext context}) async {
Utils.showLoading(context);
try {
GenericRespModel genericRespModel = await commonRepo
@ -1148,8 +1132,7 @@ class AppointmentsVM extends BaseVM {
}
}
Future<ProviderContactInfoModel?> getProvidersContactInfo(
{required int providerId, required BuildContext context}) async {
Future<ProviderContactInfoModel?> getProvidersContactInfo({required int providerId, required BuildContext context}) async {
Utils.showLoading(context);
try {
ProviderContactInfoModel contactInfoModel = await commonRepo
@ -1179,7 +1162,8 @@ class AppointmentsVM extends BaseVM {
Services services = await branchRepo.fetchServicesByCategoryId(
serviceCategoryId: -1);
services.data?.forEach((element) {
branchesCategoriesFilterOptions.add(FilterListModel(id: element.id ?? 0,
branchesCategoriesFilterOptions.add(FilterListModel(
id: element.id ?? 0,
isSelected: false,
iconUrl: element.serviceIconUrl ?? "",
title: element.description ?? "",
@ -1191,14 +1175,12 @@ class AppointmentsVM extends BaseVM {
setState(ViewState.idle);
}
Future<void> getAllNearBranches(
{bool isNeedToRebuild = false, bool isFromRefresh = false}) async {
Future<void> getAllNearBranches({bool isNeedToRebuild = false, bool isFromRefresh = false}) async {
nearbyBranches.clear();
if (isNeedToRebuild) setState(ViewState.busy);
if (isFromRefresh) {
var selectedFilter = branchesCategoriesFilterOptions.firstWhere((
element) => element.isSelected);
var selectedFilter = branchesCategoriesFilterOptions.firstWhere((element) => element.isSelected);
log("selectedFilter: ${selectedFilter.id}");
if (selectedFilter.id == 0) {
@ -1305,8 +1287,7 @@ class AppointmentsVM extends BaseVM {
// Provider Filter
List<DropValue> branchFilterProviderSearchHistory = [];
void removeBranchFilterProviderSearchHistory(
{bool isClear = false, required int index}) {
void removeBranchFilterProviderSearchHistory({bool isClear = false, required int index}) {
if (isClear) {
branchFilterProviderSearchHistory.clear();
notifyListeners();
@ -1349,8 +1330,7 @@ class AppointmentsVM extends BaseVM {
// Category Filter
List<DropValue> branchFilterCategorySearchHistory = [];
void removeBranchFilterCategorySearchHistory(
{bool isClear = false, required int index}) {
void removeBranchFilterCategorySearchHistory({bool isClear = false, required int index}) {
if (isClear) {
branchFilterCategorySearchHistory.clear();
notifyListeners();
@ -1393,8 +1373,7 @@ class AppointmentsVM extends BaseVM {
// Services Filter
List<DropValue> branchFilterServicesSearchHistory = [];
void removeBranchFilterServicesSearchHistory(
{bool isClear = false, required int index}) {
void removeBranchFilterServicesSearchHistory({bool isClear = false, required int index}) {
if (isClear) {
branchFilterServicesSearchHistory.clear();
notifyListeners();
@ -1448,8 +1427,7 @@ class AppointmentsVM extends BaseVM {
return false;
}
ifAlreadyExistForStringValue(
{required List<String> list, required String value}) {
ifAlreadyExistForStringValue({required List<String> list, required String value}) {
int index = list.indexWhere((element) {
return element == value;
});
@ -1627,8 +1605,7 @@ class AppointmentsVM extends BaseVM {
setState(ViewState.idle);
}
Future<void> getBranchesBasedOnCategoryFilters(
{required int serviceId}) async {
Future<void> getBranchesBasedOnCategoryFilters({required int serviceId}) async {
setState(ViewState.busy);
if (serviceId == 0) {
await getAllNearBranches();
@ -1647,8 +1624,7 @@ class AppointmentsVM extends BaseVM {
List<DropValue> branchesDropList = [];
Future<void> fetchAllBranchesBySelectedProviderId(
{required List<int> providersIdsList}) async {
Future<void> fetchAllBranchesBySelectedProviderId({required List<int> providersIdsList}) async {
branchesDropList.clear();
setOnlyState(ViewState.busy);
List<BranchDetailModel> providers = await branchRepo.getBranchesByFilters(
@ -1682,8 +1658,7 @@ class AppointmentsVM extends BaseVM {
// Provider Filter For Appointments
List<DropValue> appointmentFilterProviderSearchHistory = [];
void removeAppointmentFilterProviderSearchHistory(
{bool isClear = false, required int index}) {
void removeAppointmentFilterProviderSearchHistory({bool isClear = false, required int index}) {
if (isClear) {
appointmentFilterProviderSearchHistory.clear();
notifyListeners();
@ -1740,8 +1715,7 @@ class AppointmentsVM extends BaseVM {
List<DropValue> appointmentFilterBranchSearchHistory = [];
void removeAppointmentFilterBranchSearchHistory(
{bool isClear = false, required int index}) {
void removeAppointmentFilterBranchSearchHistory({bool isClear = false, required int index}) {
if (isClear) {
appointmentFilterBranchSearchHistory.clear();
notifyListeners();
@ -1784,8 +1758,7 @@ class AppointmentsVM extends BaseVM {
List<DropValue> appointmentFilterCategorySearchHistory = [];
void removeAppointmentFilterCategorySearchHistory(
{bool isClear = false, required int index}) {
void removeAppointmentFilterCategorySearchHistory({bool isClear = false, required int index}) {
if (isClear) {
appointmentFilterCategorySearchHistory.clear();
notifyListeners();
@ -1826,8 +1799,7 @@ class AppointmentsVM extends BaseVM {
List<DropValue> appointmentFilterServicesSearchHistory = [];
void removeAppointmentFilterServicesSearchHistory(
{bool isClear = false, required int index}) {
void removeAppointmentFilterServicesSearchHistory({bool isClear = false, required int index}) {
if (isClear) {
appointmentFilterServicesSearchHistory.clear();
notifyListeners();
@ -1868,8 +1840,7 @@ class AppointmentsVM extends BaseVM {
List<DropValue> appointmentFilterItemsSearchHistory = [];
void removeAppointmentFilterItemsSearchHistory(
{bool isClear = false, required int index}) {
void removeAppointmentFilterItemsSearchHistory({bool isClear = false, required int index}) {
if (isClear) {
appointmentFilterItemsSearchHistory.clear();
notifyListeners();
@ -1918,8 +1889,7 @@ class AppointmentsVM extends BaseVM {
notifyListeners();
}
void removeAppointmentFilterCustomerNameSearchHistory(
{bool isClear = false, required int index}) {
void removeAppointmentFilterCustomerNameSearchHistory({bool isClear = false, required int index}) {
if (isClear) {
appointmentFilterCustomerNameSearchHistory.clear();
notifyListeners();
@ -1957,8 +1927,7 @@ class AppointmentsVM extends BaseVM {
List<String> appointmentFilterMobilePhoneSearchHistory = [];
void removeAppointmentFilterMobilePhoneSearchHistory(
{bool isClear = false, required int index}) {
void removeAppointmentFilterMobilePhoneSearchHistory({bool isClear = false, required int index}) {
if (isClear) {
appointmentFilterMobilePhoneSearchHistory.clear();
notifyListeners();
@ -1989,8 +1958,7 @@ class AppointmentsVM extends BaseVM {
List<DropValue> appointmentFilterServiceDeliverySearchHistory = [];
void removeAppointmentFilterServiceDeliverySearchHistory(
{bool isClear = false, required int index}) {
void removeAppointmentFilterServiceDeliverySearchHistory({bool isClear = false, required int index}) {
if (isClear) {
appointmentFilterServiceDeliverySearchHistory.clear();
notifyListeners();
@ -2004,8 +1972,7 @@ class AppointmentsVM extends BaseVM {
notifyListeners();
}
void addAppointmentFilterServiceDeliverySearchHistory(
{required DropValue value}) {
void addAppointmentFilterServiceDeliverySearchHistory({required DropValue value}) {
if (appointmentFilterServiceDeliverySearchHistory.isEmpty) {
updateAppointmentFiltersCounter(appointmentFiltersCounter + 1);
}
@ -2020,8 +1987,7 @@ class AppointmentsVM extends BaseVM {
void updateAppointmentFilterSelectedServiceDeliveryId(SelectionModel id,
{bool isForSearch = false}) async {
if (isForSearch) {
DropValue itemsDrop = serviceDeliveryTypesDropList.firstWhere((
element) => element.id == id.selectedId);
DropValue itemsDrop = serviceDeliveryTypesDropList.firstWhere((element) => element.id == id.selectedId);
if (!ifAlreadyExist(list: appointmentFilterServiceDeliverySearchHistory,
value: itemsDrop)) {
addAppointmentFilterServiceDeliverySearchHistory(value: itemsDrop);
@ -2069,8 +2035,7 @@ class AppointmentsVM extends BaseVM {
notifyListeners();
}
Future<List<AppointmentListModel>> getAppointmentsBasedOnFiltersForCustomer(
{required AppointmentStatusEnum appointmentStatusEnum}) async {
Future<List<AppointmentListModel>> getAppointmentsBasedOnFiltersForCustomer({required AppointmentStatusEnum appointmentStatusEnum}) async {
try {
setState(ViewState.busy);
List<String> providersIdsList = [];
@ -2126,8 +2091,7 @@ class AppointmentsVM extends BaseVM {
String selectedDateForAppointments = "";
Future<List<AppointmentListModel>> getAppointmentsBasedOnFiltersForProviders(
{required int branchID, AppointmentStatusEnum? appointmentStatus}) async {
Future<List<AppointmentListModel>> getAppointmentsBasedOnFiltersForProviders({required int branchID, AppointmentStatusEnum? appointmentStatus}) async {
try {
setState(ViewState.busy);
List<String> servicesIdsList = [];
@ -2258,8 +2222,7 @@ class AppointmentsVM extends BaseVM {
notifyListeners();
}
Future<void> getReviewsByBranchId(
{required int serviceProviderBranchID}) async {
Future<void> getReviewsByBranchId({required int serviceProviderBranchID}) async {
setState(ViewState.busy);
try {
currentBranchReviews = await branchRepo.getBranchRatings(
@ -2274,8 +2237,7 @@ class AppointmentsVM extends BaseVM {
// Chat Related
Future<void> getGeneralChatsListForProvider(
{required BuildContext context}) async {
Future<void> getGeneralChatsListForProvider({required BuildContext context}) async {
try {
// Utils.showLoading(context);
// List<BuyersChatForAdzsModel> buyersChatList = await appointmentRepo.getChatBuyersForAds(adsID: adsID);
@ -2290,8 +2252,7 @@ class AppointmentsVM extends BaseVM {
}
}
void onGeneralChatMessagesButtonPressed(
{required BuildContext context}) async {
void onGeneralChatMessagesButtonPressed({required BuildContext context}) async {
final myUserID = AppState().getUser.data!.userInfo!.userId;
// if (adDetailsModel.isMyAd != null && adDetailsModel.isMyAd!) {
// await getGeneralChatsListForProvider(context: context);

@ -33,6 +33,7 @@ class ChatVM extends BaseVM {
ChatVM({required this.chatRepo, required this.requestRepo, required this.commonServices});
HubConnection? hubConnection;
bool _isSubscribed = false; // Track if listeners are subscribed
String chatMessageText = "";
@ -281,30 +282,42 @@ class ChatVM extends BaseVM {
}
int _retryCount = 0;
final int _maxRetries = 5; // Maximum number of retries
final int _maxRetries = 5; // Maximum number of consecutive retries before increasing delay
bool _isReconnecting = false;
Future<void> _reconnectWithBackoff(BuildContext context) async {
if (_retryCount >= _maxRetries) {
logger.e("Maximum retry attempts reached. Stopping reconnection.");
return;
Future<void> _reconnectWithBackoff() async {
// Never stop trying, but increase delays after max retries
int delaySeconds;
if (_retryCount < _maxRetries) {
delaySeconds = 2 * (_retryCount + 1); // 2, 4, 6, 8, 10 seconds
} else {
delaySeconds = 30; // After 5 failures, retry every 30 seconds indefinitely
}
final delay = Duration(seconds: 2 * (_retryCount + 1)); // Exponential backoff
logger.i("Retrying connection in ${delay.inSeconds} seconds...");
await Future.delayed(delay);
logger.i("Retrying connection in $delaySeconds seconds (attempt ${_retryCount + 1})...");
await Future.delayed(Duration(seconds: delaySeconds));
_retryCount++;
await buildHubConnection(context); // Try reconnecting
await buildHubConnection(); // Try reconnecting (no context needed)
}
Future<void> buildHubConnection(BuildContext context) async {
if (hubConnection == null || hubConnection!.state != HubConnectionState.connected) {
if (_isReconnecting) {
logger.i("Reconnection already in progress");
return;
// Clear all existing listeners to prevent duplicates
void _clearAllListeners() {
if (hubConnection != null && _isSubscribed) {
try {
hubConnection!.off(SignalrConsts.receiveMessageRequestOffer);
hubConnection!.off(SignalrConsts.receiveMessageAds);
hubConnection!.off(SignalrConsts.receiveMessageGeneral);
_isSubscribed = false;
logger.i("Cleared all SignalR listeners");
} catch (e) {
logger.e("Error clearing listeners: ${e.toString()}");
}
}
}
Future<void> buildHubConnection({BuildContext? context}) async {
if (hubConnection == null || hubConnection!.state != HubConnectionState.connected) {
if (_isReconnecting) {
logger.i("Reconnection already in progress");
return;
@ -313,34 +326,58 @@ class ChatVM extends BaseVM {
try {
_isReconnecting = true;
// Create new hub connection
hubConnection = await chatRepo.getHubConnection();
await hubConnection!.start();
// Start with timeout
await hubConnection!.start()?.timeout(
const Duration(seconds: 30),
onTimeout: () {
throw Exception("Hub connection timeout");
},
);
// Reset retry count on successful connection
_retryCount = 0;
_isReconnecting = false;
// Subscribe to messages
_subscribeToReceiveRequestOfferMessages(context);
_subscribeToReceiveAdMessages(context);
_subscribeToReceiveGeneralMessages(context);
logger.i("Hub Connected Successfully: ${hubConnection!.connectionId}");
// 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);
_isSubscribed = true;
}
// Configure handlers
hubConnection!.onclose((exception) async {
logger.i("onClose: ${exception.toString()}");
await _reconnectWithBackoff(context);
_isSubscribed = false;
await _reconnectWithBackoff(); // No context needed
});
hubConnection!.onreconnecting((exception) {
logger.i("onReconnecting: ${exception?.toString()}");
_isSubscribed = false;
});
hubConnection!.onreconnected((connectionId) {
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
});
} catch (e) {
logger.e("Error during hub connection setup: ${e.toString()}");
_isReconnecting = false; // Reset on failure
_isReconnecting = false;
// Retry on failure
await _reconnectWithBackoff();
}
notifyListeners();
@ -350,8 +387,18 @@ class ChatVM extends BaseVM {
}
Future<void> closeHubConnection() async {
if (hubConnection != null && hubConnection!.state == HubConnectionState.connected) {
hubConnection!.stop();
try {
_clearAllListeners();
if (hubConnection != null && hubConnection!.state == HubConnectionState.connected) {
await hubConnection!.stop();
logger.i("Hub connection closed");
}
hubConnection = null;
_isSubscribed = false;
_retryCount = 0;
_isReconnecting = false;
} catch (e) {
logger.e("Error closing hub connection: ${e.toString()}");
}
}
@ -369,9 +416,19 @@ class ChatVM extends BaseVM {
required List<dynamic> offerImages,
required BuildContext context,
}) async {
// Ensure connection is established
if (hubConnection == null || hubConnection!.state != HubConnectionState.connected) {
logger.i("Hub Not Connected!!");
await buildHubConnection(context);
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.");
return false;
}
}
if (hubConnection!.state == HubConnectionState.connected) {
@ -398,17 +455,20 @@ class ChatVM extends BaseVM {
};
logger.i(obj);
hubConnection!.invoke(
SignalrConsts.sendMessageRequestOffer,
args: <Object>[obj],
).catchError((e) {
logger.i("error in invoking SendMessageRequestOffer: ${e.toString()}");
Utils.showToast(e.toString());
try {
await hubConnection!.invoke(
SignalrConsts.sendMessageRequestOffer,
args: <Object>[obj],
);
return true;
} catch (e) {
logger.e("error in invoking SendMessageRequestOffer: ${e.toString()}");
Utils.showToast("Failed to send offer");
return false;
});
}
}
return true;
return false;
}
MessageImageModel convertFileToMessageImageModel({required File file}) {
@ -467,10 +527,21 @@ 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!!");
await buildHubConnection(context);
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.");
return false;
}
}
if (hubConnection!.state == HubConnectionState.connected) {
final userId = AppState().getUser.data!.userInfo!.userId.toString();
final name = AppState().getUser.data!.userInfo!.firstName.toString();
@ -495,40 +566,42 @@ class ChatVM extends BaseVM {
}
logger.i(obj);
hubConnection!.invoke(
SignalrConsts.sendMessageRequestOffer,
args: <Object>[obj],
).catchError((e) {
logger.i("error in invoking SendMessage: ${e.toString()}");
Utils.showToast(e.toString());
return false;
});
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 {
int providerIndex = context.read<RequestsVM>().myFilteredRequests.indexWhere((element) => (element.customerUserID == receiverId && element.id == requestId));
log("providerIndex2:$providerIndex");
if (providerIndex != -1) {
context.read<RequestsVM>().addChatMessagesInRequestsModel(msg: chatMessageModel, index: providerIndex);
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 {
int providerIndex = context.read<RequestsVM>().myFilteredRequests.indexWhere((element) => (element.customerUserID == receiverId && element.id == requestId));
log("providerIndex2:$providerIndex");
if (providerIndex != -1) {
context.read<RequestsVM>().addChatMessagesInRequestsModel(msg: chatMessageModel, index: providerIndex);
}
}
return true;
} catch (e) {
logger.e("error in invoking SendMessage: ${e.toString()}");
Utils.showToast("Failed to send message");
return false;
}
return true;
}
return false;
}
@ -664,7 +737,7 @@ class ChatVM extends BaseVM {
if (hubConnection == null || hubConnection!.state != HubConnectionState.connected) {
logger.i("Hub Not Connected!!");
await buildHubConnection(context);
await buildHubConnection(context: context);
}
if (hubConnection!.state == HubConnectionState.connected) {
var obj = <String, dynamic>{
@ -764,9 +837,19 @@ 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!!");
await buildHubConnection(context);
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.");
return false;
}
}
if (hubConnection!.state == HubConnectionState.connected) {
@ -781,32 +864,34 @@ class ChatVM extends BaseVM {
};
logger.i("$obj");
hubConnection!.invoke(
SignalrConsts.sendMessageAds,
args: <Object>[obj],
).catchError((e) {
logger.i("error in invoking SendMessage: ${e.toString()}");
Utils.showToast(e.toString());
try {
await hubConnection!.invoke(
SignalrConsts.sendMessageAds,
args: <Object>[obj],
);
ChatMessageModel chatMessageModel = ChatMessageModel(
messageType: chatMessageType.getIdFromChatMessageTypeEnum(),
chatText: message,
isMyMessage: true,
senderName: name,
senderUserID: userId,
chatMessageTypeEnum: ChatMessageTypeEnum.freeText,
receiverUserID: receiverId,
);
currentMessagesForAds.add(chatMessageModel);
notifyListeners();
if (AppState().currentAppType == AppType.customer) {
} else {}
return true;
} catch (e) {
logger.e("error in invoking SendMessage: ${e.toString()}");
Utils.showToast("Failed to send message");
return false;
});
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;
}
}
return false;
}
@ -843,7 +928,7 @@ class ChatVM extends BaseVM {
Future<void> onNewMessageReceivedForGeneral({required List<ChatMessageModel> messages}) async {
for (var msg in messages) {
currentMessagesForAds.add(msg);
currentMessagesForGeneralChat.add(msg); // ✅ FIXED: Use correct list
}
scrollChatDown();
notifyListeners();
@ -872,9 +957,19 @@ 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!!");
await buildHubConnection(context);
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.");
return false;
}
}
if (hubConnection!.state == HubConnectionState.connected) {
@ -887,31 +982,33 @@ class ChatVM extends BaseVM {
};
logger.i("$obj");
hubConnection!.invoke(
SignalrConsts.sendMessageGeneral,
args: <Object>[obj],
).catchError((e) {
logger.i("error in invoking SendMessage: ${e.toString()}");
Utils.showToast(e.toString());
try {
await hubConnection!.invoke(
SignalrConsts.sendMessageGeneral,
args: <Object>[obj],
);
ChatMessageModel chatMessageModel = ChatMessageModel(
messageType: chatMessageType.getIdFromChatMessageTypeEnum(),
chatText: message,
isMyMessage: true,
senderName: name,
senderUserID: userId,
chatMessageTypeEnum: ChatMessageTypeEnum.freeText,
receiverUserID: receiverId,
);
currentMessagesForGeneralChat.add(chatMessageModel);
notifyListeners();
if (AppState().currentAppType == AppType.customer) {
} else {}
return true;
} catch (e) {
logger.e("error in invoking SendMessage: ${e.toString()}");
Utils.showToast("Failed to send message");
return false;
});
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;
}
}
return false;
}

@ -1,5 +1,6 @@
import 'dart:developer';
import 'dart:io';
import 'package:flutter/widgets.dart';
import 'package:mc_common_app/classes/app_state.dart';
import 'package:mc_common_app/generated/locale_keys.g.dart';
import 'package:mc_common_app/main.dart';
@ -11,17 +12,59 @@ 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/appointments_view_model.dart';
import 'package:mc_common_app/view_models/base_view_model.dart';
import 'package:flutter/cupertino.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:mc_common_app/view_models/chat_view_model.dart';
import 'package:mc_common_app/view_models/requests_view_model.dart';
import 'package:provider/provider.dart';
class DashboardVmCustomer extends BaseVM {
class DashboardVmCustomer extends BaseVM with WidgetsBindingObserver {
final CommonAppServices commonServices;
final UserRepo userRepo;
BuildContext? _context; // Store context for lifecycle events
DashboardVmCustomer({required this.commonServices, required this.userRepo});
DashboardVmCustomer({required this.commonServices, required this.userRepo}) {
// Register as lifecycle observer
WidgetsBinding.instance.addObserver(this);
}
// Store context when dashboard is initialized
void setContext(BuildContext context) {
_context = context;
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
_context = null;
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
logger.i("App lifecycle state changed to: $state");
if (_context == null) return;
final chatVM = Provider.of<ChatVM>(_context!, listen: false);
switch (state) {
case AppLifecycleState.resumed:
// App came back to foreground - ensure SignalR is connected
logger.i("App resumed - checking SignalR connection");
chatVM.buildHubConnection(context: _context);
break;
case AppLifecycleState.paused:
// App went to background - keep connection alive (as per user requirement)
logger.i("App paused - SignalR staying connected");
break;
case AppLifecycleState.inactive:
case AppLifecycleState.detached:
case AppLifecycleState.hidden:
// Other states - no action needed
break;
}
}
String pickedImage = "";
@ -81,7 +124,7 @@ class DashboardVmCustomer extends BaseVM {
await requestsVM.getRequests();
await adVM.getVehicleTypes();
await adVM.getVehicleAdsDuration();
await chatVM.buildHubConnection(context);
await chatVM.buildHubConnection(context: context);
// if (appointmentsVM.myAppointments.isEmpty) {
// await appointmentsVM.getMyAppointments();

@ -1,7 +1,7 @@
import 'dart:io';
import 'package:flutter/widgets.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/cupertino.dart';
import 'package:mc_common_app/classes/app_state.dart';
import 'package:mc_common_app/config/dependency_injection.dart';
import 'package:mc_common_app/config/routes.dart';
@ -25,11 +25,54 @@ import 'package:mc_common_app/view_models/requests_view_model.dart';
import 'package:mc_common_app/view_models/service_view_model.dart';
import 'package:mc_common_app/view_models/subscriptions_view_model.dart';
class DashboardVMProvider extends BaseVM {
class DashboardVMProvider extends BaseVM with WidgetsBindingObserver {
final CommonAppServices commonServices;
final UserRepo userRepo;
BuildContext? _context; // Store context for lifecycle events
DashboardVMProvider({required this.commonServices, required this.userRepo});
DashboardVMProvider({required this.commonServices, required this.userRepo}) {
// Register as lifecycle observer
WidgetsBinding.instance.addObserver(this);
}
// Store context when dashboard is initialized
void setContext(BuildContext context) {
_context = context;
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
_context = null;
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
logger.i("App lifecycle state changed to: $state");
if (_context == null) return;
final chatVM = Provider.of<ChatVM>(_context!, listen: false);
switch (state) {
case AppLifecycleState.resumed:
// App came back to foreground - ensure SignalR is connected
logger.i("App resumed - checking SignalR connection");
chatVM.buildHubConnection(context: _context);
break;
case AppLifecycleState.paused:
// App went to background - keep connection alive (as per user requirement)
logger.i("App paused - SignalR staying connected");
break;
case AppLifecycleState.inactive:
case AppLifecycleState.detached:
case AppLifecycleState.hidden:
// Other states - no action needed
break;
}
}
String pickedImage = "";
@ -102,7 +145,7 @@ class DashboardVMProvider extends BaseVM {
await adVM.getVehicleTypes();
await adVM.getVehicleAdsDuration();
await chatVM.buildHubConnection(context);
await chatVM.buildHubConnection(context: context);
}
void checkUserSubscription(SubscriptionActionTypeEnum actionType, BuildContext context, {required Function() callback}) async {

@ -45,6 +45,9 @@ class RequestsVM extends BaseVM {
List<FilterListModel> requestsTypeFilterOptions = [];
// Spare parts condition filters (All, New, Used)
List<FilterListModel> sparePartsConditionFilterOptions = [];
List<EnumsModel> myRequestsTypeEnum = [];
List<EnumsModel> vehicleConditionsEnum = [];
@ -157,9 +160,24 @@ class RequestsVM extends BaseVM {
requestCityIdApi = vehicleCityId.selectedId;
}
int requestConditionIdApi = 0;
if (vehicleConditionId.selectedId != -1) {
log("DEBUG - requestTypeIdApi: $requestTypeIdApi");
log("DEBUG - sparePartsConditionFilterOptions.isNotEmpty: ${sparePartsConditionFilterOptions.isNotEmpty}");
if (sparePartsConditionFilterOptions.isNotEmpty) {
log("DEBUG - sparePartsConditionFilterOptions: ${sparePartsConditionFilterOptions.map((e) => '${e.title}(id:${e.id}, selected:${e.isSelected})').toList()}");
}
// Use condition filter for all request types
if (sparePartsConditionFilterOptions.isNotEmpty) {
// Get selected condition (All=3, New=1, Used=2)
requestConditionIdApi = sparePartsConditionFilterOptions.firstWhere((element) => element.isSelected).id;
log("DEBUG - Using condition filter: $requestConditionIdApi");
} else if (vehicleConditionId.selectedId != -1) {
// Fallback to regular vehicle condition filter if condition filters not initialized
requestConditionIdApi = vehicleConditionId.selectedId;
log("DEBUG - Using vehicle condition filter: $requestConditionIdApi");
}
int requestVehicleYearIdApi = 0;
if (vehicleYearId.selectedId != -1) {
requestVehicleYearIdApi = vehicleYearId.selectedId;
@ -168,6 +186,7 @@ class RequestsVM extends BaseVM {
if (requestStatusId.selectedId != -1) {
requestStatusIdApi = requestStatusId.selectedId;
}
log("requestConditionIdApi:${requestConditionIdApi}");
try {
myFilteredRequests = await requestRepo.getRequestsBasedOnFilters(
@ -257,17 +276,52 @@ class RequestsVM extends BaseVM {
notifyListeners();
}
applyFilterOnRequestsVM({required RequestsTypeEnum requestsTypeEnum}) async {
applyFilterOnRequestsVM({required RequestsTypeEnum requestsTypeEnum, int? conditionId}) async {
if (requestsTypeFilterOptions.isEmpty) return;
for (var value in requestsTypeFilterOptions) {
value.isSelected = false;
}
requestsTypeFilterOptions[requestsTypeEnum.getIdFromRequestTypeEnum() - 1].isSelected = true; // -1 to match with the index
// Set condition filter: use provided conditionId or default to "All"
if (sparePartsConditionFilterOptions.isNotEmpty) {
for (var value in sparePartsConditionFilterOptions) {
value.isSelected = false;
}
if (conditionId != null && conditionId != -1) {
// Find and select the matching condition
int index = sparePartsConditionFilterOptions.indexWhere((element) => element.id == conditionId);
if (index != -1) {
sparePartsConditionFilterOptions[index].isSelected = true;
} else {
sparePartsConditionFilterOptions[0].isSelected = true; // Fallback to "All"
}
} else {
sparePartsConditionFilterOptions[0].isSelected = true; // Select "All" by default
}
}
await getRequestsBasedOnFilters();
isOfferSent = false;
notifyListeners();
}
// Apply spare parts condition filter (All, New, Used)
applySparePartsConditionFilter({required int conditionId}) async {
if (sparePartsConditionFilterOptions.isEmpty) return;
for (var value in sparePartsConditionFilterOptions) {
value.isSelected = false;
}
// Find and select the filter with matching conditionId
int index = sparePartsConditionFilterOptions.indexWhere((element) => element.id == conditionId);
if (index != -1) {
sparePartsConditionFilterOptions[index].isSelected = true;
}
log("index: $index");
await getRequestsBasedOnFilters();
notifyListeners();
}
List<ImageModel> pickedVehicleImages = [];
String vehicleImageError = "";
@ -334,6 +388,13 @@ 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
}
if (vehicleBrands.isEmpty) {
vehicleBrands = await commonRepo.getVehicleBrands(vehicleTypeId: -1);
}
@ -815,8 +876,8 @@ class RequestsVM extends BaseVM {
brand: brand,
model: model,
year: vehicleYearId.selectedOption,
isNew: vehicleConditionId.selectedId == 1,
// 1 for new, 2 for Used
conditionType: vehicleConditionId.selectedId,
// conditionType: 1 for New, 2 for Used
countryID: vehicleCountryId.selectedId,
cityID: vehicleCityId.selectedId,
price: price.isEmpty ? "1.0" : price,
@ -830,7 +891,13 @@ class RequestsVM extends BaseVM {
log("requestTypeId.selectedId.toRequestTypeEnum(): ${requestTypeId.selectedId.toRequestTypeEnum()}");
Utils.showToast(LocaleKeys.requestSuccessfullyCreated.tr());
Navigator.pop(context);
await applyFilterOnRequestsVM(requestsTypeEnum: requestTypeId.selectedId.toRequestTypeEnum());
// Apply filter to match the created request (request type + condition in one call)
await applyFilterOnRequestsVM(
requestsTypeEnum: requestTypeId.selectedId.toRequestTypeEnum(),
conditionId: vehicleConditionId.selectedId,
);
resetRequestCreationForm();
} else {
Utils.showToast(respModel.message.toString());

@ -259,6 +259,22 @@ class VehicleDetails extends StatelessWidget {
hint: LocaleKeys.odometerReading.tr(),
onChanged: (v) => adVM.updateOdometer(v),
),
8.height,
// Can mileage increase dropdown
DropdownField(
(DropValue value) => adVM.updateIsInUsed(value.id),
list: [
DropValue(1, LocaleKeys.yes.tr(), ""),
DropValue(0, LocaleKeys.no.tr(), ""),
],
dropdownValue: DropValue(
adVM.isInUsed,
adVM.isInUsed == 1 ? LocaleKeys.yes.tr() : LocaleKeys.no.tr(),
"",
),
hint: LocaleKeys.canMileageIncrease.tr(),
errorValue: "",
),
],
],
8.height,
@ -304,68 +320,31 @@ class VehicleDetails extends StatelessWidget {
onChanged: (v) => adVM.updateVehicleDescription(v),
),
22.height,
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
// Finance Available toggle
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
LocaleKeys.financeAvailable.tr().toText(fontSize: 16),
8.height,
Container(
width: 50,
height: 30,
decoration: BoxDecoration(
color: adVM.financeAvailableStatus ? MyColors.darkPrimaryColor : MyColors.white,
borderRadius: BorderRadius.circular(25.0),
border: Border.all(color: MyColors.black, width: 1),
),
child: Transform.scale(
scale: 0.8,
child: CupertinoSwitch(
activeColor: MyColors.darkPrimaryColor,
trackColor: MyColors.white,
thumbColor: MyColors.grey98Color,
value: adVM.financeAvailableStatus,
onChanged: (value) {
adVM.updateFinanceAvailableStatus(value);
},
),
),
),
],
LocaleKeys.financeAvailable.tr().toText(fontSize: 16),
8.height,
Container(
width: 50,
height: 30,
decoration: BoxDecoration(
color: adVM.financeAvailableStatus ? MyColors.darkPrimaryColor : MyColors.white,
borderRadius: BorderRadius.circular(25.0),
border: Border.all(color: MyColors.black, width: 1),
),
),
8.width,
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
LocaleKeys.currentlyInUse.tr().toText(fontSize: 16),
8.height,
Container(
width: 50,
height: 30,
decoration: BoxDecoration(
color: adVM.isInUsed == 1 ? MyColors.darkPrimaryColor : MyColors.white,
borderRadius: BorderRadius.circular(25.0),
border: Border.all(color: MyColors.black, width: 1),
),
child: Transform.scale(
scale: 0.8,
child: CupertinoSwitch(
activeColor: MyColors.darkPrimaryColor,
trackColor: MyColors.white,
thumbColor: MyColors.grey98Color,
value: adVM.isInUsed == 1,
onChanged: (value) {
adVM.updateIsInUsed(value ? 1 : 0);
},
),
),
),
],
child: Transform.scale(
scale: 0.8,
child: CupertinoSwitch(
activeColor: MyColors.darkPrimaryColor,
trackColor: MyColors.white,
thumbColor: MyColors.grey98Color,
value: adVM.financeAvailableStatus,
onChanged: (value) {
adVM.updateFinanceAvailableStatus(value);
},
),
),
),
],

@ -173,7 +173,6 @@ class _AdCardState extends State<AdCard> {
alignment: Alignment.center,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: SizedBox(
width: 100,
height: 100,
@ -281,6 +280,10 @@ class _AdCardState extends State<AdCard> {
] else if (isAdsFragment && !context.read<AdVM>().isExploreAdsTapped && widget.adDetails.statuslabel != null && widget.adDetails.statuslabel!.isNotEmpty) ...[
Utils.statusContainerChip(text: widget.adDetails.statuslabel ?? "", chipColor: Utils.getChipColorByAdStatus(widget.adDetails.adPostStatus!)),
],
// Show Accident Free chip for explore ads when isAccidentFree is true
if (isAdsFragment && context.read<AdVM>().isExploreAdsTapped && widget.adDetails.vehicle?.isAccidentFree == true) ...[
Utils.statusContainerChip(text: LocaleKeys.accidentFree.tr(), chipColor: Colors.green),
],
(widget.adDetails.vehicle!.vehicleTitle ?? "").toText(
fontSize: 16,
letterSpacing: -0.64,

@ -20,6 +20,7 @@ import 'package:mc_common_app/widgets/button/show_fill_button.dart';
import 'package:mc_common_app/widgets/common_widgets/app_bar.dart';
import 'package:mc_common_app/widgets/common_widgets/filters_list.dart';
import 'package:mc_common_app/widgets/extensions/extensions_widget.dart';
import 'package:mc_common_app/widgets/shimmer/home_shimmer_widgets.dart';
import 'package:provider/provider.dart';
class AdsFragment extends StatelessWidget {
@ -147,7 +148,7 @@ class AdsFragment extends StatelessWidget {
}
},
child: adVM.state == ViewState.busy
? const Center(child: CircularProgressIndicator())
? const AdsListShimmer(itemCount: 5)
: AdsListWidget(
isAdsFragment: true,
isDraftAds: false,

@ -19,6 +19,7 @@ import 'package:mc_common_app/widgets/button/show_fill_button.dart';
import 'package:mc_common_app/widgets/common_widgets/app_bar.dart';
import 'package:mc_common_app/widgets/common_widgets/filters_list.dart';
import 'package:mc_common_app/widgets/extensions/extensions_widget.dart';
import 'package:mc_common_app/widgets/shimmer/home_shimmer_widgets.dart';
import 'package:provider/provider.dart';
import 'package:easy_localization/easy_localization.dart';
@ -124,12 +125,22 @@ class MyRequestsFragment extends StatelessWidget {
requestsVM.applyFilterOnRequestsVM(requestsTypeEnum: selectedFilterId.toRequestTypeEnum());
},
),
// Show condition filters for all request types
if (requestsVM.sparePartsConditionFilterOptions.isNotEmpty) ...[
8.height,
FiltersList(
filterList: requestsVM.sparePartsConditionFilterOptions,
onFilterTapped: (index, selectedFilterId) {
requestsVM.applySparePartsConditionFilter(conditionId: selectedFilterId);
},
),
],
8.height,
Expanded(
child: RefreshIndicator(
onRefresh: () async => await requestsVM.getRequestsBasedOnFilters(),
child: requestsVM.state == ViewState.busy
? const Center(child: CircularProgressIndicator())
? const RequestsListShimmer(itemCount: 5)
: requestsVM.myFilteredRequests.isEmpty
? Column(
mainAxisAlignment: MainAxisAlignment.center,

@ -53,7 +53,7 @@ class SplashPageState extends State<SplashPage> with WidgetsBindingObserver {
super.didChangeAppLifecycleState(state);
if (state == AppLifecycleState.resumed) {
final chatVM = context.read<ChatVM>();
chatVM.buildHubConnection(context);
chatVM.buildHubConnection(context: context);
}
}

@ -75,12 +75,7 @@ class _DropdownFieldState extends State<DropdownField> {
// Show disabled text field when not selectable
if (!widget.isSelectAble)
Container(
decoration: Utils.containerColorRadiusBorderWidth(
MyColors.greyShadowColor,
0,
MyColors.greyACColor,
2
),
decoration: Utils.containerColorRadiusBorderWidth(MyColors.greyShadowColor, 0, MyColors.greyACColor, 2),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14),
width: double.infinity,
child: (dropdownValue?.value ?? "").toText(
@ -90,16 +85,9 @@ class _DropdownFieldState extends State<DropdownField> {
),
)
else
// Show normal dropdown when selectable
// Show normal dropdown when selectable
Container(
decoration: widget.showAppointmentPickerVariant
? null
: Utils.containerColorRadiusBorderWidth(
MyColors.white,
0,
MyColors.darkPrimaryColor,
2
),
decoration: widget.showAppointmentPickerVariant ? null : Utils.containerColorRadiusBorderWidth(MyColors.white, 0, MyColors.darkPrimaryColor, 2),
margin: const EdgeInsets.all(0),
padding: const EdgeInsets.only(left: 8, right: 8),
width: widget.showAppointmentPickerVariant ? 170 : null,
@ -127,15 +115,11 @@ class _DropdownFieldState extends State<DropdownField> {
},
onTap: widget.onTap,
items: (widget.list ?? defaultV).map<DropdownMenuItem<DropValue>>(
(DropValue value) {
(DropValue value) {
return DropdownMenuItem<DropValue>(
value: value,
enabled: value.isEnabled ?? true,
child: value.value.toText(
fontSize: 15,
color: value.isEnabled == false ? MyColors.darkTextColor : null,
fontWeight: MyFonts.Medium
),
child: value.value.toText(fontSize: 15, color: value.isEnabled == false ? MyColors.darkTextColor : null, fontWeight: MyFonts.Medium),
);
},
).toList(),

@ -371,7 +371,10 @@ extension BuildSVG on String? {
return SizedBox(
height: height,
width: width,
child: Image.asset(MyAssets.brokenImage).paddingAll(10),
child: Image.asset(
MyAssets.brokenImage,
fit: BoxFit.fill,
),
);
}
@ -382,7 +385,11 @@ extension BuildSVG on String? {
height: height,
width: width,
decoration: BoxDecoration(border: Border.all(color: MyColors.roundedCrossBgColor)),
child: const Icon(Icons.image_not_supported_outlined, color: MyColors.roundedCrossBgColor),
child: Icon(
Icons.image_not_supported_outlined,
color: MyColors.roundedCrossBgColor,
size: width,
),
);
},
loadingBuilder: (BuildContext context, Widget? child, ImageChunkEvent? imageChunk) {

@ -0,0 +1,228 @@
import 'package:flutter/material.dart';
import 'package:shimmer/shimmer.dart';
/// Shimmer for horizontal appointment slider
class AppointmentSliderShimmer extends StatelessWidget {
const AppointmentSliderShimmer({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return SizedBox(
height: 180,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: 3,
padding: const EdgeInsets.symmetric(horizontal: 21),
itemBuilder: (context, index) {
return Shimmer.fromColors(
baseColor: Colors.grey[300]!,
highlightColor: Colors.grey[100]!,
child: Container(
width: MediaQuery.of(context).size.width * 0.85,
margin: EdgeInsets.only(right: index < 2 ? 12 : 0),
decoration: const BoxDecoration(
color: Colors.white,
),
),
);
},
),
);
}
}
/// Shimmer for appointments list (vertical list of appointment cards)
class AppointmentsListShimmer extends StatelessWidget {
final int itemCount;
const AppointmentsListShimmer({super.key, this.itemCount = 5});
@override
Widget build(BuildContext context) {
return Shimmer.fromColors(
baseColor: Colors.grey[300]!,
highlightColor: Colors.grey[100]!,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 21),
child: ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: itemCount,
separatorBuilder: (context, index) => const SizedBox(height: 12),
itemBuilder: (context, index) {
return Container(
height: 140,
decoration: const BoxDecoration(
color: Colors.white,
),
);
},
),
),
);
}
}
/// Shimmer for branches list (vertical list of branch cards)
class BranchesListShimmer extends StatelessWidget {
final int itemCount;
const BranchesListShimmer({Key? key, this.itemCount = 5}) : super(key: key);
@override
Widget build(BuildContext context) {
return Shimmer.fromColors(
baseColor: Colors.grey[300]!,
highlightColor: Colors.grey[100]!,
child: Padding(
padding: const EdgeInsets.all(12),
child: ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: itemCount,
separatorBuilder: (context, index) => const SizedBox(height: 12),
itemBuilder: (context, index) {
return Container(
height: 160,
decoration: const BoxDecoration(
color: Colors.white,
),
);
},
),
),
);
}
}
/// Shimmer for requests list (vertical list of request cards)
class RequestsListShimmer extends StatelessWidget {
final int itemCount;
const RequestsListShimmer({Key? key, this.itemCount = 5}) : super(key: key);
@override
Widget build(BuildContext context) {
return Shimmer.fromColors(
baseColor: Colors.grey[300]!,
highlightColor: Colors.grey[100]!,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 21),
child: ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: itemCount,
separatorBuilder: (context, index) => const SizedBox(height: 12),
itemBuilder: (context, index) {
return Container(
height: 120,
decoration: const BoxDecoration(
color: Colors.white,
),
);
},
),
),
);
}
}
/// Shimmer for services grid (3 columns)
class ServicesGridShimmer extends StatelessWidget {
const ServicesGridShimmer({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Shimmer.fromColors(
baseColor: Colors.grey[300]!,
highlightColor: Colors.grey[100]!,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 21),
child: GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
mainAxisSpacing: 12,
crossAxisSpacing: 12,
childAspectRatio: 0.9,
),
itemCount: 6,
itemBuilder: (context, index) {
return Container(
decoration: const BoxDecoration(
color: Colors.white,
),
);
},
),
),
);
}
}
/// Shimmer for ads list
class AdsListShimmer extends StatelessWidget {
final int itemCount;
const AdsListShimmer({Key? key, this.itemCount = 3}) : super(key: key);
@override
Widget build(BuildContext context) {
return Shimmer.fromColors(
baseColor: Colors.grey[300]!,
highlightColor: Colors.grey[100]!,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 21),
child: ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: itemCount,
separatorBuilder: (context, index) => const SizedBox(height: 12),
itemBuilder: (context, index) {
return Container(
height: 140,
decoration: const BoxDecoration(
color: Colors.white,
),
);
},
),
),
);
}
}
/// Shimmer for provider branches grid
class BranchesGridShimmer extends StatelessWidget {
const BranchesGridShimmer({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Shimmer.fromColors(
baseColor: Colors.grey[300]!,
highlightColor: Colors.grey[100]!,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 21),
child: GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 12,
crossAxisSpacing: 12,
childAspectRatio: 0.75,
),
itemCount: 4,
itemBuilder: (context, index) {
return Container(
decoration: const BoxDecoration(
color: Colors.white,
),
);
},
),
),
);
}
}

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save