From ba0c2c56e7bdb809643a19f3b02e0c07b2f521b9 Mon Sep 17 00:00:00 2001 From: FaizHashmi Date: Thu, 8 Oct 2026 10:18:07 +0300 Subject: [PATCH] Bug Fixes --- lib/classes/consts.dart | 2 +- lib/repositories/ads_repo.dart | 4 +- lib/repositories/appointment_repo.dart | 5 +- lib/view_models/ad_view_model.dart | 89 +++++++++++++------ lib/view_models/appointments_view_model.dart | 28 ++++++ .../dashboard_view_model_provider.dart | 5 ++ lib/view_models/user_view_model.dart | 5 ++ .../components/ads_list_widget.dart | 9 +- lib/views/common_fragments/ads_fragment.dart | 8 ++ lib/widgets/button/show_fill_button.dart | 2 +- 10 files changed, 121 insertions(+), 36 deletions(-) diff --git a/lib/classes/consts.dart b/lib/classes/consts.dart index 76e72d8..8310756 100644 --- a/lib/classes/consts.dart +++ b/lib/classes/consts.dart @@ -215,7 +215,7 @@ class GlobalConsts { // When false, API logs print only the request URL and payload. When true, the // response body is also printed. Flip this on only while actively debugging a // specific API issue -- flip back off afterwards, since response bodies are noisy. - static bool showApiResponseInLogs = false; + static bool showApiResponseInLogs = true; static String appNameCustomer = "MOWATER Customer"; static String appNameProvider = "MOWATER Provider"; diff --git a/lib/repositories/ads_repo.dart b/lib/repositories/ads_repo.dart index 0e58ecf..2ccd8eb 100644 --- a/lib/repositories/ads_repo.dart +++ b/lib/repositories/ads_repo.dart @@ -416,7 +416,7 @@ class AdsRepoImp implements AdsRepo { "CityIDs": cityIdsList ?? [], "VehicleTypeIDs": vehicleTypeIdsList ?? [], "VehicleBrandIDs": vehicleBrandIdsList ?? [], - "VehicleModelIDs": vehicleModelIdsList ?? [], + "VehicleModels": vehicleModelIdsList ?? [], "VehicleModelYearIDs": vehicleModelYearIdsList ?? [], "CreatedByRoles": createdByRolesIdsList ?? [], "AdsStatuses": ["${AdPostStatus.active.getIdFromAdPostStatusEnum()}"], //only Active ADS @@ -509,7 +509,7 @@ class AdsRepoImp implements AdsRepo { statusMap[reserved.adsID ?? 0] = (reserved.adsReserveStatus ?? 0).toAdRserveStatusEnum(); reserveIdMap[reserved.adsID ?? 0] = reserved.id ?? 0; // Store reserve ID - } + } log("📋 Status Map created: ${statusMap.length} entries"); statusMap.forEach((key, value) { diff --git a/lib/repositories/appointment_repo.dart b/lib/repositories/appointment_repo.dart index 7c5f3ef..3e8b98b 100644 --- a/lib/repositories/appointment_repo.dart +++ b/lib/repositories/appointment_repo.dart @@ -251,8 +251,8 @@ class AppointmentRepoImp implements AppointmentRepo { String providerId = appState.getUser.data!.userInfo!.providerId.toString(); var params = { "ServiceProviderID": providerId.toString(), - "ProviderBranchID": branchID.toString(), "IsSearchFilter": true.toString(), + "ProviderBranchID": branchID.toString(), "SPServiceIDs": serviceProviderServiceIdsList ?? [], "CustomerNames": customerNamesList ?? [], "CustomerMobileNos": customerPhonesList ?? [], @@ -260,6 +260,9 @@ class AppointmentRepoImp implements AppointmentRepo { "FromDate": fromDate ?? "", "DateTo": toDate ?? "", }; + // 0 means "all branches" (e.g. the home dashboard carousel) -- omit the key entirely + // rather than sending "0", since branch IDs are real backend records starting at 1 + // and "0" likely just doesn't match any branch, returning an empty result. if (appointmentStatusEnum != null) { params.addAll({"AppointmentStatusID": appointmentStatusEnum.getIdFromAppointmentStatusEnum().toString()}); diff --git a/lib/view_models/ad_view_model.dart b/lib/view_models/ad_view_model.dart index acf39b1..7b51ddd 100644 --- a/lib/view_models/ad_view_model.dart +++ b/lib/view_models/ad_view_model.dart @@ -1526,7 +1526,7 @@ class AdVM extends BaseVM { final isArabic = AppState().isArabic(context); actionConfirmationBottomSheet( context: context, - title:( isArabic ? "حفظ كمسودة" : "Save As Draft").toText(fontSize: 28, isBold: true, letterSpacing: -1.44), + title: (isArabic ? "حفظ كمسودة" : "Save As Draft").toText(fontSize: 28, isBold: true, letterSpacing: -1.44), subtitle: isArabic ? "سيتم حفظ إعلانك كمسودة، ويمكنك المتابعة لاحقًا من حيث توقفت. هل تريد الحفظ والرجوع؟" : "Your ad will be saved as draft and you can continue later from where you left off. Do you want to save and go back?", @@ -2056,6 +2056,22 @@ class AdVM extends BaseVM { isAccidentFree = false; isAccidentFreeSet = false; accidentFreeError = ""; + // These error strings aren't part of a SelectionModel (unlike the dropdowns reset + // above), so they were never cleared here -- confirmed on a real device: once + // isVehicleDetailsValidated() set one of these, it stayed set and showed up on the + // very next ad creation, before the user had touched anything. + demandAmountError = ""; + vehicleVinError = ""; + vehicleTitleError = ""; + warrantyError = ""; + vehicleDescError = ""; + vehicleImageError = ""; + // Edit-mode flags also need resetting -- isAdEditEnabled in particular gates the + // minimum-image-count check in isVehicleDetailsValidated(), so leaving it stale as + // true would silently skip that requirement for a genuinely new ad. + isAdEditEnabled = false; + isDraftEditEnabled = false; + isExtendAdEditEnabled = false; clearSpecialServiceCard(); updateFinanceAvailableStatus(false); notifyListeners(); @@ -3000,40 +3016,59 @@ class AdVM extends BaseVM { await updateSelectionVehicleBrandId(SelectionModel(selectedId: vehicleBrands[index].id!, selectedOption: vehicleBrands[index].vehicleBrandDescription ?? "")); } updateSelectionVehicleModelId(SelectionModel(selectedId: previousAdDetails!.vehicle!.model!.id!, selectedOption: previousAdDetails!.vehicle!.model!.label ?? "")); - updateSelectionVehicleModelYearId(SelectionModel(selectedId: previousAdDetails!.vehicle!.modelyear!.id!, selectedOption: previousAdDetails!.vehicle!.modelyear!.label ?? "")); - updateSelectionVehicleColorId(SelectionModel( - selectedId: previousAdDetails!.vehicle!.color!.id!, selectedOption: isArabic ? previousAdDetails!.vehicle!.color!.labelN.toString() : previousAdDetails!.vehicle!.color!.label ?? "")); - updateSelectionVehicleConditionId(SelectionModel( - selectedId: previousAdDetails!.vehicle!.condition!.id!, - selectedOption: isArabic ? previousAdDetails!.vehicle!.condition!.labelN.toString() : previousAdDetails!.vehicle!.condition!.label ?? "")); - updateSelectionVehicleCategoryId(SelectionModel( - selectedId: previousAdDetails!.vehicle!.category!.id!, selectedOption: isArabic ? previousAdDetails!.vehicle!.category!.labelN.toString() : previousAdDetails!.vehicle!.category!.label ?? "")); - updateSelectionVehicleMileageId(SelectionModel( - selectedId: previousAdDetails!.vehicle!.mileage!.id!, - selectedOption: "${previousAdDetails!.vehicle!.mileage!.mileageStart} - ${previousAdDetails!.vehicle!.mileage!.mileageEnd}", - )); - updateSelectionVehicleTransmissionId(SelectionModel( - selectedId: previousAdDetails!.vehicle!.transmission!.id!, - selectedOption: isArabic ? previousAdDetails!.vehicle!.transmission!.labelN.toString() : previousAdDetails!.vehicle!.transmission!.label ?? "")); - updateSelectionVehicleSellerTypeId(SelectionModel( - selectedId: previousAdDetails!.vehicle!.sellertype!.id!, - selectedOption: isArabic ? previousAdDetails!.vehicle!.sellertype!.labelN.toString() : previousAdDetails!.vehicle!.sellertype!.label ?? "")); + // Confirmed from a real draft API response: an unset nested field comes back as a + // non-null placeholder object ({"id": 0, "label": null, ...}), not an absent/null + // field -- same "0 means not set" convention the demandAmount/odometer/warantyYears + // fields below already account for. Blindly trusting .id! here (without this check) + // set selectedId to 0 instead of -1, which silently passed the `selectedId == -1` + // required-field check on Next even though nothing was selected. Always assigning + // (real value OR an explicit empty SelectionModel) rather than conditionally skipping + // also avoids leaking a stale selection left over from a previous ad-editing session. + updateSelectionVehicleModelYearId((previousAdDetails!.vehicle!.modelyear?.id ?? 0) > 0 + ? SelectionModel(selectedId: previousAdDetails!.vehicle!.modelyear!.id!, selectedOption: previousAdDetails!.vehicle!.modelyear!.label ?? "") + : SelectionModel(selectedOption: "", selectedId: -1, errorValue: "")); + updateSelectionVehicleColorId((previousAdDetails!.vehicle!.color?.id ?? 0) > 0 + ? SelectionModel( + selectedId: previousAdDetails!.vehicle!.color!.id!, selectedOption: isArabic ? previousAdDetails!.vehicle!.color!.labelN.toString() : previousAdDetails!.vehicle!.color!.label ?? "") + : SelectionModel(selectedOption: "", selectedId: -1, errorValue: "")); + updateSelectionVehicleConditionId((previousAdDetails!.vehicle!.condition?.id ?? 0) > 0 + ? SelectionModel( + selectedId: previousAdDetails!.vehicle!.condition!.id!, + selectedOption: isArabic ? previousAdDetails!.vehicle!.condition!.labelN.toString() : previousAdDetails!.vehicle!.condition!.label ?? "") + : SelectionModel(selectedOption: "", selectedId: -1, errorValue: "")); + updateSelectionVehicleCategoryId((previousAdDetails!.vehicle!.category?.id ?? 0) > 0 + ? SelectionModel( + selectedId: previousAdDetails!.vehicle!.category!.id!, + selectedOption: isArabic ? previousAdDetails!.vehicle!.category!.labelN.toString() : previousAdDetails!.vehicle!.category!.label ?? "") + : SelectionModel(selectedOption: "", selectedId: -1, errorValue: "")); + updateSelectionVehicleMileageId((previousAdDetails!.vehicle!.mileage?.id ?? 0) > 0 + ? SelectionModel( + selectedId: previousAdDetails!.vehicle!.mileage!.id!, + selectedOption: "${previousAdDetails!.vehicle!.mileage!.mileageStart} - ${previousAdDetails!.vehicle!.mileage!.mileageEnd}", + ) + : SelectionModel(selectedOption: "", selectedId: -1, errorValue: "")); + updateSelectionVehicleTransmissionId((previousAdDetails!.vehicle!.transmission?.id ?? 0) > 0 + ? SelectionModel( + selectedId: previousAdDetails!.vehicle!.transmission!.id!, + selectedOption: isArabic ? previousAdDetails!.vehicle!.transmission!.labelN.toString() : previousAdDetails!.vehicle!.transmission!.label ?? "") + : SelectionModel(selectedOption: "", selectedId: -1, errorValue: "")); + updateSelectionVehicleSellerTypeId((previousAdDetails!.vehicle!.sellertype?.id ?? 0) > 0 + ? SelectionModel( + selectedId: previousAdDetails!.vehicle!.sellertype!.id!, + selectedOption: isArabic ? previousAdDetails!.vehicle!.sellertype!.labelN.toString() : previousAdDetails!.vehicle!.sellertype!.label ?? "") + : SelectionModel(selectedOption: "", selectedId: -1, errorValue: "")); int indexCountry = vehicleCountries.indexWhere((element) => element.id == previousAdDetails!.vehicle!.countryID); if (indexCountry != -1) { updateSelectionVehicleCountryId(SelectionModel(selectedId: vehicleCountries[indexCountry].id!, selectedOption: vehicleCountries[indexCountry].countryName ?? "")); } updateSelectionVehicleCityId(SelectionModel(selectedId: previousAdDetails!.vehicle!.cityID!, selectedOption: previousAdDetails!.vehicle!.cityName ?? "")); - vehicleDemandAmount = (previousAdDetails!.vehicle!.demandAmount == null || previousAdDetails!.vehicle!.demandAmount == 0) - ? "" - : previousAdDetails!.vehicle!.demandAmount!.toInt().toString(); + vehicleDemandAmount = (previousAdDetails!.vehicle!.demandAmount == null || previousAdDetails!.vehicle!.demandAmount == 0) ? "" : previousAdDetails!.vehicle!.demandAmount!.toInt().toString(); vehicleVin = previousAdDetails!.vehicle!.vehicleVIN ?? ""; vehicleTitle = previousAdDetails!.vehicle!.vehicleTitle ?? ""; vehicleDescription = previousAdDetails!.vehicle!.vehicleDescription ?? ""; financeAvailableStatus = previousAdDetails!.vehicle!.isFinanceAvailable ?? false; - odometer = (previousAdDetails!.vehicle!.odometer == null || previousAdDetails!.vehicle!.odometer == 0) - ? "" - : previousAdDetails!.vehicle!.odometer.toString(); + odometer = (previousAdDetails!.vehicle!.odometer == null || previousAdDetails!.vehicle!.odometer == 0) ? "" : previousAdDetails!.vehicle!.odometer.toString(); isInUsed = previousAdDetails!.vehicle!.isInUsed ?? 0; isAccidentFree = previousAdDetails!.vehicle!.isAccidentFree ?? false; isAccidentFreeSet = true; @@ -3098,9 +3133,7 @@ class AdVM extends BaseVM { adPhoneNumber = previousAdDetails!.adOwnerDetails!.mobileNo ?? ""; isNumberOnWhatsApp = previousAdDetails!.whatsAppNo != null; - warrantyDuration = (previousAdDetails!.warrantyYears == null || previousAdDetails!.warrantyYears == "0") - ? "" - : previousAdDetails!.warrantyYears!; + warrantyDuration = (previousAdDetails!.warrantyYears == null || previousAdDetails!.warrantyYears == "0") ? "" : previousAdDetails!.warrantyYears!; notifyListeners(); } diff --git a/lib/view_models/appointments_view_model.dart b/lib/view_models/appointments_view_model.dart index 851cc28..b112003 100644 --- a/lib/view_models/appointments_view_model.dart +++ b/lib/view_models/appointments_view_model.dart @@ -67,6 +67,34 @@ class AppointmentsVM extends BaseVM { List appointmentsFilterOptions = []; List myFilteredAppointmentsForProvider = []; + // AppointmentsVM is a single long-lived Provider instance for the app's whole lifetime, + // not recreated per login -- confirmed on a real device that logging out and back in as + // a different user left the previous user's selectedBranchIdForAppointments (and search + // filter history) in place, silently scoping the new user's appointments/home carousel + // to a branch that belongs to someone else's account. Call this from logout. + void resetForNewSession() { + selectedAppointmentIndex = 0; + selectedAppointmentSubIndex = 0; + selectedAppointmentId = 0; + selectedBranchIdForAppointments = 0; + myAppointments = []; + myUpComingAppointments = []; + myFilteredAppointmentsForCustomers = []; + myFilteredAppointmentsForProvider = []; + branchFilterProviderSearchHistory = []; + branchFilterCategorySearchHistory = []; + branchFilterServicesSearchHistory = []; + appointmentFilterProviderSearchHistory = []; + appointmentFilterBranchSearchHistory = []; + appointmentFilterCategorySearchHistory = []; + appointmentFilterServicesSearchHistory = []; + appointmentFilterItemsSearchHistory = []; + appointmentFilterCustomerNameSearchHistory = []; + appointmentFilterMobilePhoneSearchHistory = []; + appointmentFilterServiceDeliverySearchHistory = []; + appointmentFiltersCounter = 0; + } + bool isFetchingServices = false; List branchCategories = []; diff --git a/lib/view_models/dashboard_view_model_provider.dart b/lib/view_models/dashboard_view_model_provider.dart index e52b974..edb316f 100644 --- a/lib/view_models/dashboard_view_model_provider.dart +++ b/lib/view_models/dashboard_view_model_provider.dart @@ -159,6 +159,11 @@ class DashboardVMProvider extends BaseVM with WidgetsBindingObserver { requestsVM.getRequests(), subscriptionsVM.getSubscriptionBySP(AppState().getUser.data?.userInfo?.providerId.toString() ?? "", true), appointmentVM.applyFilterOnAppointmentsVMForProviders( + // Explicit 0 (all branches) rather than relying on the branchID default -- + // AppointmentsVM is a single long-lived instance, so if the user previously + // viewed a specific branch's appointments, selectedBranchIdForAppointments would + // still be set to that branch and silently scope the home carousel to it. + branchID: 0, appointmentStatusEnum: AppointmentStatusEnum.allAppointments, isNeedCustomerFilter: true, shouldPopulateUpcoming: true, diff --git a/lib/view_models/user_view_model.dart b/lib/view_models/user_view_model.dart index e5e8e9d..c4d25e1 100644 --- a/lib/view_models/user_view_model.dart +++ b/lib/view_models/user_view_model.dart @@ -16,6 +16,7 @@ import 'package:mc_common_app/main.dart'; import 'package:mc_common_app/models/general_models/generic_resp_model.dart'; import 'package:mc_common_app/models/general_models/generic_resp_model.dart'; import 'package:mc_common_app/models/subscriptions_models/subscription_model.dart'; +import 'package:mc_common_app/view_models/appointments_view_model.dart'; import 'package:mc_common_app/models/user_models/basic_otp.dart'; import 'package:mc_common_app/models/user_models/change_email.dart'; import 'package:mc_common_app/models/user_models/change_mobile.dart'; @@ -763,6 +764,10 @@ class UserVM extends BaseVM { SharedPrefManager.setPhoneOrEmail(""); SharedPrefManager.setUserPassword(""); context.read().closeHubConnection(); + // AppointmentsVM is a single long-lived instance, not recreated per login -- without + // this, the next user to log in on this device would inherit the previous user's + // selected branch filter and cached appointment lists. + context.read().resetForNewSession(); if (AppState().getUser.data!.userInfo!.userLocalImage != null) { AppState().getUser.data!.userInfo!.userLocalImage = null; AppState().getUser.data!.userInfo!.userImageUrl = null; diff --git a/lib/views/advertisement/components/ads_list_widget.dart b/lib/views/advertisement/components/ads_list_widget.dart index f1d5318..6463553 100644 --- a/lib/views/advertisement/components/ads_list_widget.dart +++ b/lib/views/advertisement/components/ads_list_widget.dart @@ -245,13 +245,16 @@ class _AdCardState extends State { ), ), if (widget.adDetails.adPostStatus == AdPostStatus.sold) ...[ - // Blur overlay + // Blur overlay -- sized to match the 80x80 image box above (not + // 100x100): Stack sizes itself to its largest non-positioned child, so + // a bigger overlay here was silently growing the whole image area only + // for sold ads. ClipRRect( child: BackdropFilter( filter: ImageFilter.blur(sigmaX: 3, sigmaY: 3), child: Container( - width: 100, - height: 100, + width: 80, + height: 80, color: Colors.black.withOpacity(0.3), ), ), diff --git a/lib/views/common_fragments/ads_fragment.dart b/lib/views/common_fragments/ads_fragment.dart index e0d7b08..564a68c 100644 --- a/lib/views/common_fragments/ads_fragment.dart +++ b/lib/views/common_fragments/ads_fragment.dart @@ -196,6 +196,14 @@ class AdsFragment extends StatelessWidget { ), floatingActionButton: FloatingActionButton( onPressed: () async { + // AdVM is a long-lived instance, not recreated per ad -- without this, a + // brand new ad could start showing validation error messages and/or field + // selections left over from a previous draft/cancelled ad in the same + // session. previousAdDetails = null makes sure this is treated as a genuinely + // new ad, not an edit (isCreateNew checks in saveAdAsDraft rely on this). + adVM.resetValues(); + adVM.previousAdDetails = null; + // ✅ Await API calls with loading state await adVM.getVehicleAdsDuration(); diff --git a/lib/widgets/button/show_fill_button.dart b/lib/widgets/button/show_fill_button.dart index fdc6b8f..0060d36 100644 --- a/lib/widgets/button/show_fill_button.dart +++ b/lib/widgets/button/show_fill_button.dart @@ -79,7 +79,7 @@ class ShowFillButton extends StatelessWidget { : null), margin: EdgeInsets.symmetric(horizontal: horizontalMargin, vertical: verticalMargin), child: MaterialButton( - onPressed: (isLoading || onPressed == null) ? null : onPressed, + onPressed: (isLoading || isDisabled || onPressed == null) ? null : onPressed, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(radius), side: isFilled ? BorderSide.none : BorderSide(width: 2, color: borderColor),