diff --git a/.gitignore b/.gitignore index a8e938c..5bf292d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ + # Miscellaneous *.class *.log diff --git a/assets/langs/ar-SA.json b/assets/langs/ar-SA.json index 6c78f09..7bcb156 100644 --- a/assets/langs/ar-SA.json +++ b/assets/langs/ar-SA.json @@ -150,6 +150,7 @@ "my_service_providers": "مزودي الخدمة الجدد", "appointment_details": "تفاصيل الموعد", "no_city_available": "لا توجد مدينة متاحة لهذا البلد", + "noDataAvailable": "لا توجد بيانات متاحة", "branch_created": "تم إنشاء الفرع بنجاح", "branch_updated": "تم تحديث الفرع بنجاح", "branch_deleted": "تم حذف الفرع بنجاح", @@ -334,6 +335,7 @@ "vehiclePictures": "صور المركبة", "vehiclePart": "جزء المركبة", "damagePartPictures": "صور الأجزاء المتضررة", + "qrCode": "رمز الاستجابة السريعة", "vehicleDamagePart": "جزء المركبة المتضرر", "duration": "المدة", "specialServices": "الخدمات الخاصة", @@ -385,6 +387,7 @@ "error": "خطأ", "appointmentBookedSuccessfully": "تم حجز موعدك بنجاح", "appointmentStatusUpdated": "تم تحديث حالة الموعد", + "appointmentStatusLabel": "حالة الموعد", "paymentStatusUpdated": "تم تحديث حالة الدفع", "serviceStatusUpdated": "تم تحديث حالة الخدمة", "selectOneSubService": "يرجى اختيار خدمة فرعية واحدة على الأقل", @@ -664,6 +667,7 @@ "searchByRequestedDate": "البحث حسب تاريخ الطلب", "selectDate": "اختر التاريخ", "searchByVehicleModel": "البحث حسب طراز المركبة", + "selectVehicleModel": "اختر طراز المركبة", "customerCarePrompt": "هل لديك أي استفسار؟ نحن هنا لخدمتك. انقر هنا للتحدث مع خدمة العملاء لدينا الآن.", "callNow": "اتصل الآن", "condition": "الحالة", diff --git a/assets/langs/en-US.json b/assets/langs/en-US.json index a55fec8..61f5814 100644 --- a/assets/langs/en-US.json +++ b/assets/langs/en-US.json @@ -153,6 +153,7 @@ "my_service_providers": "My Recent Service Providers", "appointment_details": "Appointment Details", "no_city_available": "No City Available for this country", + "noDataAvailable": "No Data Available", "branch_created": "Branch is successfully created", "branch_updated": "Branch is successfully Updated", "branch_deleted": "Branch is successfully Deleted", @@ -337,6 +338,7 @@ "vehiclePictures": "Vehicle Pictures", "vehiclePart": "Vehicle Part", "damagePartPictures": "Damage Part Pictures", + "qrCode": "QR Code", "vehicleDamagePart": "Vehicle Damage Part", "duration": "Duration", "specialServices": "Special Services", @@ -388,6 +390,7 @@ "error": "Error", "appointmentBookedSuccessfully": "Your appointment has been booked successfully", "appointmentStatusUpdated": "Appointment Status Updated", + "appointmentStatusLabel": "Appointment Status", "paymentStatusUpdated": "Payment Status Updated", "serviceStatusUpdated": "Service Status Updated", "selectOneSubService": "Please select at least one sub service", @@ -668,6 +671,7 @@ "searchByRequestedDate": "Search by Requested Date", "selectDate": "Select Date", "searchByVehicleModel": "Search By Vehicle Model", + "selectVehicleModel": "Select Vehicle Model", "customerCarePrompt": "Got any question? We are here to service. Click here to talk to our customer care right now.", "callNow": "Call Now", "condition": "Condition", diff --git a/lib/api/api_client.dart b/lib/api/api_client.dart index 54aeebb..65a3218 100644 --- a/lib/api/api_client.dart +++ b/lib/api/api_client.dart @@ -124,13 +124,14 @@ class ApiClientImp implements ApiClient { return await _postForResponse(url, requestBody, token: token, queryParameters: queryParameters, headers: headers, retryTimes: retryTimes); } - Future _postForResponse(String url, - requestBody, { - String? token, - Map? queryParameters, - Map? headers, - int retryTimes = 0, - }) async { + Future _postForResponse( + String url, + requestBody, { + String? token, + Map? queryParameters, + Map? headers, + int retryTimes = 0, + }) async { try { var headers0 = {}; @@ -158,7 +159,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) { @@ -210,8 +211,7 @@ class ApiClientImp implements ApiClient { bool _certificateCheck(X509Certificate cert, String host, int port) => true; Future _withClient(Future Function(Client) fn) async { - var httpClient = HttpClient() - ..badCertificateCallback = _certificateCheck; + var httpClient = HttpClient()..badCertificateCallback = _certificateCheck; var client = IOClient(httpClient); try { return await fn(client); @@ -271,8 +271,10 @@ class ApiClientImp implements ApiClient { } if (!kReleaseMode) { - logger.i(url); - logger.i("$queryParameters"); + log("---URL---"); + log(url); + log("---Params---"); + log("$queryParameters"); } var response = await _get(Uri.parse(url), headers: headers0).timeout(const Duration(seconds: 120)); diff --git a/lib/generated/codegen_loader.g.dart b/lib/generated/codegen_loader.g.dart index dd0545c..12d6329 100644 --- a/lib/generated/codegen_loader.g.dart +++ b/lib/generated/codegen_loader.g.dart @@ -166,6 +166,7 @@ class CodegenLoader extends AssetLoader{ "my_service_providers": "مزودي الخدمة الجدد", "appointment_details": "تفاصيل الموعد", "no_city_available": "لا توجد مدينة متاحة لهذا البلد", + "noDataAvailable": "لا توجد بيانات متاحة", "branch_created": "تم إنشاء الفرع بنجاح", "branch_updated": "تم تحديث الفرع بنجاح", "branch_deleted": "تم حذف الفرع بنجاح", @@ -350,6 +351,7 @@ class CodegenLoader extends AssetLoader{ "vehiclePictures": "صور المركبة", "vehiclePart": "جزء المركبة", "damagePartPictures": "صور الأجزاء المتضررة", + "qrCode": "رمز الاستجابة السريعة", "vehicleDamagePart": "جزء المركبة المتضرر", "duration": "المدة", "specialServices": "الخدمات الخاصة", @@ -401,6 +403,7 @@ class CodegenLoader extends AssetLoader{ "error": "خطأ", "appointmentBookedSuccessfully": "تم حجز موعدك بنجاح", "appointmentStatusUpdated": "تم تحديث حالة الموعد", + "appointmentStatusLabel": "حالة الموعد", "paymentStatusUpdated": "تم تحديث حالة الدفع", "serviceStatusUpdated": "تم تحديث حالة الخدمة", "selectOneSubService": "يرجى اختيار خدمة فرعية واحدة على الأقل", @@ -680,6 +683,7 @@ class CodegenLoader extends AssetLoader{ "searchByRequestedDate": "البحث حسب تاريخ الطلب", "selectDate": "اختر التاريخ", "searchByVehicleModel": "البحث حسب طراز المركبة", + "selectVehicleModel": "اختر طراز المركبة", "customerCarePrompt": "هل لديك أي استفسار؟ نحن هنا لخدمتك. انقر هنا للتحدث مع خدمة العملاء لدينا الآن.", "callNow": "اتصل الآن", "condition": "الحالة", @@ -1088,6 +1092,7 @@ static const Map _en_US = { "my_service_providers": "My Recent Service Providers", "appointment_details": "Appointment Details", "no_city_available": "No City Available for this country", + "noDataAvailable": "No Data Available", "branch_created": "Branch is successfully created", "branch_updated": "Branch is successfully Updated", "branch_deleted": "Branch is successfully Deleted", @@ -1272,6 +1277,7 @@ static const Map _en_US = { "vehiclePictures": "Vehicle Pictures", "vehiclePart": "Vehicle Part", "damagePartPictures": "Damage Part Pictures", + "qrCode": "QR Code", "vehicleDamagePart": "Vehicle Damage Part", "duration": "Duration", "specialServices": "Special Services", @@ -1323,6 +1329,7 @@ static const Map _en_US = { "error": "Error", "appointmentBookedSuccessfully": "Your appointment has been booked successfully", "appointmentStatusUpdated": "Appointment Status Updated", + "appointmentStatusLabel": "Appointment Status", "paymentStatusUpdated": "Payment Status Updated", "serviceStatusUpdated": "Service Status Updated", "selectOneSubService": "Please select at least one sub service", @@ -1603,6 +1610,7 @@ static const Map _en_US = { "searchByRequestedDate": "Search by Requested Date", "selectDate": "Select Date", "searchByVehicleModel": "Search By Vehicle Model", + "selectVehicleModel": "Select Vehicle Model", "customerCarePrompt": "Got any question? We are here to service. Click here to talk to our customer care right now.", "callNow": "Call Now", "condition": "Condition", diff --git a/lib/generated/locale_keys.g.dart b/lib/generated/locale_keys.g.dart index 25bf87d..97ac4e3 100644 --- a/lib/generated/locale_keys.g.dart +++ b/lib/generated/locale_keys.g.dart @@ -132,6 +132,7 @@ abstract class LocaleKeys { static const my_service_providers = 'my_service_providers'; static const appointment_details = 'appointment_details'; static const no_city_available = 'no_city_available'; + static const noDataAvailable = 'noDataAvailable'; static const branch_created = 'branch_created'; static const branch_updated = 'branch_updated'; static const branch_deleted = 'branch_deleted'; @@ -316,6 +317,7 @@ abstract class LocaleKeys { static const vehiclePictures = 'vehiclePictures'; static const vehiclePart = 'vehiclePart'; static const damagePartPictures = 'damagePartPictures'; + static const qrCode = 'qrCode'; static const vehicleDamagePart = 'vehicleDamagePart'; static const duration = 'duration'; static const specialServices = 'specialServices'; @@ -366,6 +368,7 @@ abstract class LocaleKeys { static const error = 'error'; static const appointmentBookedSuccessfully = 'appointmentBookedSuccessfully'; static const appointmentStatusUpdated = 'appointmentStatusUpdated'; + static const appointmentStatusLabel = 'appointmentStatusLabel'; static const paymentStatusUpdated = 'paymentStatusUpdated'; static const serviceStatusUpdated = 'serviceStatusUpdated'; static const selectOneSubService = 'selectOneSubService'; @@ -645,6 +648,7 @@ abstract class LocaleKeys { static const searchByRequestedDate = 'searchByRequestedDate'; static const selectDate = 'selectDate'; static const searchByVehicleModel = 'searchByVehicleModel'; + static const selectVehicleModel = 'selectVehicleModel'; static const customerCarePrompt = 'customerCarePrompt'; static const callNow = 'callNow'; static const condition = 'condition'; diff --git a/lib/models/advertisment_models/ad_details_model.dart b/lib/models/advertisment_models/ad_details_model.dart index e169d3e..fefc8d3 100644 --- a/lib/models/advertisment_models/ad_details_model.dart +++ b/lib/models/advertisment_models/ad_details_model.dart @@ -14,6 +14,8 @@ class AdDetailsModel { // List? reserved; int? statusID; + + //TODO: Overwrite these with StatusLabelDisplay and StatusLabelDisplayN String? statuslabel; String? statuslabelN; double? adsDurationPrice; @@ -137,8 +139,8 @@ class AdDetailsModel { }); } statusID = json['statusID']; - statuslabel = json['statuslabel']; - statuslabelN = json['statuslabelN']; + statuslabel = json['statuslabelDisplay'] ?? json['statuslabel']; + statuslabelN = json['statuslabelDisplayN'] ?? json['statuslabelN'] ?? json['statuslabelDisplay'] ?? json['statuslabel']; adsDurationPrice = json['adsDurationPrice']; adsDurationDiscount = json['adsDurationDiscount']; adsDurationDiscountPrice = json['adsDurationDiscountPrice']; diff --git a/lib/models/general_models/enums_model.dart b/lib/models/general_models/enums_model.dart index 5413d41..6d792cf 100644 --- a/lib/models/general_models/enums_model.dart +++ b/lib/models/general_models/enums_model.dart @@ -20,8 +20,10 @@ class EnumsModel { factory EnumsModel.fromJson(Map json) => EnumsModel( id: json["id"], enumTypeId: json["enumTypeID"], - enumValueStr: json["enumValueStr"], - enumValueStrN: json["enumValueStrN"] ?? json["enumValueStr"], + // enumValueStr: json["enumValueStr"], + // enumValueStrN: json["enumValueStrN"] ?? json["enumValueStr"], + enumValueStr: json["enumValueDisplay"] ?? json["enumValueStr"], + enumValueStrN: json["enumValueDisplayN"] ?? json["enumValueDisplay"] ?? json["enumValueStrN"] ?? json["enumValueStr"], enumValueStrDes: json["enumValueStrDes"] ?? json["enumValueStr"], enumValue: json["enumValue"], isActive: json["isActive"], diff --git a/lib/repositories/ads_repo.dart b/lib/repositories/ads_repo.dart index 39d9f31..3c874c4 100644 --- a/lib/repositories/ads_repo.dart +++ b/lib/repositories/ads_repo.dart @@ -44,6 +44,7 @@ abstract class AdsRepo { List? vehicleModelYearIdsList, List? vehicleTypeIdsList, List? vehicleBrandIdsList, + List? vehicleModelIdsList, List? createdByRolesIdsList, List? vehicleAdConditionIdsList, List? vehicleAdCreatedDateList, @@ -66,6 +67,8 @@ abstract class AdsRepo { Future toggleAdFavorite({required int adsID, required bool isFavorite}); + Future getAdDetailsById({required int adId}); + Future getAdBankingAccountInfo({required int adId}); Future updateAdStatus({required int adId, required AdPostStatus adStatusToUpdate, String? comment}); @@ -397,6 +400,7 @@ class AdsRepoImp implements AdsRepo { List? vehicleModelYearIdsList, List? vehicleTypeIdsList, List? vehicleBrandIdsList, + List? vehicleModelIdsList, List? createdByRolesIdsList, List? vehicleAdConditionIdsList, List? vehicleAdCreatedDateList, @@ -411,6 +415,7 @@ class AdsRepoImp implements AdsRepo { "CityIDs": cityIdsList ?? [], "VehicleTypeIDs": vehicleTypeIdsList ?? [], "VehicleBrandIDs": vehicleBrandIdsList ?? [], + "VehicleModelIDs": vehicleModelIdsList ?? [], "VehicleModelYearIDs": vehicleModelYearIdsList ?? [], "CreatedByRoles": createdByRolesIdsList ?? [], "AdsStatuses": ["${AdPostStatus.active.getIdFromAdPostStatusEnum()}"], //only Active ADS @@ -560,11 +565,7 @@ class AdsRepoImp implements AdsRepo { } @override - Future> getFavouriteAds({ - AdPostStatus? adPostStatus, - CreatedByRoleEnum? createdByRoleEnum, - int? vehicleBrandId, - }) async { + Future> getFavouriteAds({AdPostStatus? adPostStatus, CreatedByRoleEnum? createdByRoleEnum, int? vehicleBrandId}) async { // Build filter parameters similar to getAllAds Map params = { "PageSize": "30", @@ -604,9 +605,15 @@ class AdsRepoImp implements AdsRepo { @override Future toggleAdFavorite({required int adsID, required bool isFavorite}) async { var params = { - "ID": adsID.toString(), + "adsID": adsID.toString(), }; + if (isFavorite) { + params = { + "ID": adsID.toString(), + }; + } + String endpoint = isFavorite ? ApiConsts.unFavouriteAds : ApiConsts.favouriteAdsCreate; GenericRespModel genericRespModel = await apiClient.postJsonForObject( @@ -619,6 +626,18 @@ class AdsRepoImp implements AdsRepo { return genericRespModel; } + @override + Future getAdDetailsById({required int adId}) async { + try { + List ads = await getAdsPerSpecificIds(ids: [adId.toString()], reservedAds: []); + if (ads.isEmpty) return null; + return ads.first; + } catch (e) { + logger.e(e.toString()); + return null; + } + } + @override Future getAdBankingAccountInfo({required int adId}) async { var params = { diff --git a/lib/repositories/appointment_repo.dart b/lib/repositories/appointment_repo.dart index e5b2808..7c5f3ef 100644 --- a/lib/repositories/appointment_repo.dart +++ b/lib/repositories/appointment_repo.dart @@ -304,8 +304,7 @@ class AppointmentRepoImp implements AppointmentRepo { queryParameters: params, ApiConsts.customersAppointmentGetByFilters, ); - print("genericRespModel.data ......."); - print(genericRespModel.data); + List appointmentList = List.generate(genericRespModel.data.length, (index) => AppointmentListModel.fromJson(genericRespModel.data[index])); return appointmentList; } diff --git a/lib/repositories/common_repo.dart b/lib/repositories/common_repo.dart index 4169dba..55dd118 100644 --- a/lib/repositories/common_repo.dart +++ b/lib/repositories/common_repo.dart @@ -257,7 +257,7 @@ class CommonRepoImp implements CommonRepo { token: appState.getUser.data!.accessToken, (json) => GenericRespModel.fromJson(json), ApiConsts.vehicleModelGet, - queryParameters: postParams, + queryParameters: vehicleTypeId != -1 ? postParams : null, ); List vehicleModels = List.generate(adsGenericModel.data.length, (index) => VehicleModel.fromJson(adsGenericModel.data[index])); return vehicleModels; diff --git a/lib/services/ad_deep_link_service.dart b/lib/services/ad_deep_link_service.dart new file mode 100644 index 0000000..7b89b10 --- /dev/null +++ b/lib/services/ad_deep_link_service.dart @@ -0,0 +1,64 @@ +import 'dart:async'; +import 'dart:developer'; + +import 'package:app_links/app_links.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:mc_common_app/config/routes.dart'; +import 'package:mc_common_app/generated/locale_keys.g.dart'; +import 'package:mc_common_app/utils/navigator.dart'; +import 'package:mc_common_app/utils/utils.dart'; +import 'package:mc_common_app/view_models/ad_view_model.dart'; +import 'package:provider/provider.dart'; + +/// Handles opening the ad detail screen directly when the app is launched or +/// resumed via a shared ad link (e.g. https://ms.hmg.com/ad/1541), for both +/// Android App Links and iOS Universal Links. +class AdDeepLinkService { + static final AppLinks _appLinks = AppLinks(); + static StreamSubscription? _subscription; + + /// Call once, after the app's first frame (so [navigatorKey] is attached to a Navigator). + static Future init() async { + try { + final Uri? initialUri = await _appLinks.getInitialLink(); + if (initialUri != null) { + _handleUri(initialUri); + } + } catch (e) { + log("AdDeepLinkService: failed to read initial link - $e"); + } + + _subscription?.cancel(); + _subscription = _appLinks.uriLinkStream.listen( + _handleUri, + onError: (Object e) => log("AdDeepLinkService: stream error - $e"), + ); + } + + static void _handleUri(Uri uri) { + log("AdDeepLinkService: received $uri"); + final segments = uri.pathSegments; + if (segments.length < 2 || segments[0] != 'ad') return; + + final int? adId = int.tryParse(segments[1]); + if (adId == null) return; + + _openAd(adId); + } + + static Future _openAd(int adId) async { + final context = navigatorKey.currentContext; + if (context == null) return; + + Utils.showLoading(context); + final adDetails = await context.read().getAdDetailsById(adId); + Utils.hideLoading(context); + + if (adDetails == null) { + Utils.showToast(LocaleKeys.somethingWrong.tr()); + return; + } + + navigateWithName(context, AppRoutes.adsDetailView, arguments: adDetails); + } +} diff --git a/lib/view_models/ad_view_model.dart b/lib/view_models/ad_view_model.dart index 3542a72..b997e77 100644 --- a/lib/view_models/ad_view_model.dart +++ b/lib/view_models/ad_view_model.dart @@ -378,6 +378,15 @@ class AdVM extends BaseVM { } } + Future getAdDetailsById(int adId) async { + try { + return await adsRepo.getAdDetailsById(adId: adId); + } catch (e) { + logger.e(e.toString()); + return null; + } + } + Future getExploreAds() async { pageIndexForExploreAds = 1; hasMoreDataForExploreAds = true; @@ -727,6 +736,7 @@ class AdVM extends BaseVM { if (!ifAlreadyExist(list: vehicleBrandsAdSearchHistory, value: brandValue)) { addToVehicleBrandsAdSearchHistory(value: brandValue); } + pruneVehicleModelsAdSearchHistoryForRemovedBrands(); notifyListeners(); return; } @@ -739,7 +749,16 @@ class AdVM extends BaseVM { SelectionModel vehicleModelId = SelectionModel(selectedOption: "", selectedId: -1, errorValue: ""); - void updateSelectionVehicleModelId(SelectionModel id) { + void updateSelectionVehicleModelId(SelectionModel id, {bool isForSearch = false}) { + if (isForSearch) { + VehicleModel model = vehicleModelsForSelectedBrandsFilter.firstWhere((element) => element.id == id.selectedId); + DropValue modelValue = DropValue(model.id ?? 0, model.model ?? "", ""); + if (!ifAlreadyExist(list: vehicleModelsAdSearchHistory, value: modelValue)) { + addToVehicleModelsAdSearchHistory(value: modelValue); + } + notifyListeners(); + return; + } vehicleModelId = id; notifyListeners(); } @@ -1250,7 +1269,7 @@ class AdVM extends BaseVM { isInUsedError = ""; } - if ((pickedPostingImages.isEmpty || pickedPostingImages.length < 3) && !isAdEditEnabled) { + if (pickedPostingImages.isEmpty || (pickedPostingImages.length < 3 && !isAdEditEnabled)) { vehicleImageError = GlobalConsts.attachImageError; isValidated = false; } else { @@ -2321,6 +2340,15 @@ class AdVM extends BaseVM { List vehicleTypesForFilters = []; + List vehicleModelsForFilters = []; + + // Cascading list: only models belonging to the brand(s) currently selected in vehicleBrandsAdSearchHistory. + List get vehicleModelsForSelectedBrandsFilter { + if (vehicleBrandsAdSearchHistory.isEmpty) return []; + Set selectedBrandIds = vehicleBrandsAdSearchHistory.map((e) => e.id).toSet(); + return vehicleModelsForFilters.where((model) => selectedBrandIds.contains(model.vehicleBrandID)).toList(); + } + Future populateDataForAdFilter() async { setState(ViewState.busy); pageIndexForExploreAds = 1; @@ -2349,6 +2377,10 @@ class AdVM extends BaseVM { vehicleModelYears = await commonRepo.getVehicleModelYears(vehicleTypeId: -1); } + if (vehicleModelsForFilters.isEmpty) { + vehicleModelsForFilters = await commonRepo.getVehicleModels(vehicleTypeId: -1); // to get all the models + } + vehicleCities = await commonRepo.getVehicleCities(countryId: -1); // fetch all the cities setState(ViewState.idle); } @@ -2492,6 +2524,7 @@ class AdVM extends BaseVM { void removeVehicleBrandsAdSearchHistory({bool isClear = false, required int index}) { if (isClear) { vehicleBrandsAdSearchHistory.clear(); + pruneVehicleModelsAdSearchHistoryForRemovedBrands(); notifyListeners(); return; } @@ -2499,6 +2532,7 @@ class AdVM extends BaseVM { if (vehicleBrandsAdSearchHistory.isEmpty) { updateAdsFiltersCounter(adsFiltersCounter - 1); } + pruneVehicleModelsAdSearchHistoryForRemovedBrands(); notifyListeners(); } @@ -2510,6 +2544,42 @@ class AdVM extends BaseVM { notifyListeners(); } + // MODELS (cascades from vehicleBrandsAdSearchHistory) + List vehicleModelsAdSearchHistory = []; + + void removeVehicleModelsAdSearchHistory({bool isClear = false, required int index}) { + if (isClear) { + vehicleModelsAdSearchHistory.clear(); + notifyListeners(); + return; + } + vehicleModelsAdSearchHistory.removeAt(index); + if (vehicleModelsAdSearchHistory.isEmpty) { + updateAdsFiltersCounter(adsFiltersCounter - 1); + } + notifyListeners(); + } + + void addToVehicleModelsAdSearchHistory({required DropValue value}) { + if (vehicleModelsAdSearchHistory.isEmpty) { + updateAdsFiltersCounter(adsFiltersCounter + 1); + } + vehicleModelsAdSearchHistory.add(value); + notifyListeners(); + } + + // Removes any previously selected models whose brand is no longer present in vehicleBrandsAdSearchHistory, + // keeping the Model filter consistent whenever the Brand selection changes (add or remove). + void pruneVehicleModelsAdSearchHistoryForRemovedBrands() { + if (vehicleModelsAdSearchHistory.isEmpty) return; + Set validModelIds = vehicleModelsForSelectedBrandsFilter.map((e) => e.id ?? -1).toSet(); + int previousLength = vehicleModelsAdSearchHistory.length; + vehicleModelsAdSearchHistory.removeWhere((element) => !validModelIds.contains(element.id)); + if (vehicleModelsAdSearchHistory.isEmpty && previousLength > 0) { + updateAdsFiltersCounter(adsFiltersCounter - 1); + } + } + // LOCATION List vehicleLocationAdSearchHistory = []; @@ -2675,6 +2745,7 @@ class AdVM extends BaseVM { vehicleAdConditionSearchHistory.clear(); adFilterMobilePhoneSearchHistory.clear(); vehicleBrandsAdSearchHistory.clear(); + vehicleModelsAdSearchHistory.clear(); adFilterDemandStartPrice = ''; adFilterDemandEndPrice = ''; clearAdFilterSelections(); // Clear dropdown selections @@ -2730,6 +2801,13 @@ class AdVM extends BaseVM { } } + List modelsIdsList = []; + if (vehicleModelsAdSearchHistory.isNotEmpty) { + for (var element in vehicleModelsAdSearchHistory) { + modelsIdsList.add(element.id.toString()); + } + } + List vehicleYearIdsList = []; if (vehicleYearAdSearchHistory.isNotEmpty) { for (var element in vehicleYearAdSearchHistory) { @@ -2794,6 +2872,7 @@ class AdVM extends BaseVM { createdByRolesIdsList: adOwnerIdsList, vehicleTypeIdsList: typesIdsList, vehicleBrandIdsList: brandsIdsList, + vehicleModelIdsList: modelsIdsList, vehicleModelYearIdsList: vehicleYearIdsList, vehicleAdConditionIdsList: conditionsIdsList, vehicleAdCreatedDateList: createdDatesList, diff --git a/lib/view_models/appointments_view_model.dart b/lib/view_models/appointments_view_model.dart index 4cff667..cca1d3f 100644 --- a/lib/view_models/appointments_view_model.dart +++ b/lib/view_models/appointments_view_model.dart @@ -307,8 +307,23 @@ class AppointmentsVM extends BaseVM { notifyListeners(); } + /// Used when editing a service already added to [servicesInCurrentAppointment] (e.g. to add/remove items). + /// Points [currentServiceSelection] at the SAME object already holding this service's real state + /// (isHomeSelected, servicelocationInfo, accumulated items/price) instead of a fresh, pristine copy + /// looked up from [branchServices] — which would lose that state and desync item-level isHomeSelected. + void editServiceInCurrentAppointment(ServiceModel serviceData) { + currentServiceSelection = serviceData; + branchSelectedServiceId = SelectionModel( + selectedId: serviceData.serviceProviderServiceId ?? -1, + selectedOption: serviceData.serviceDescription ?? "", + ); + notifyListeners(); + } + void removeServiceInCurrentAppointment(int index) { - int serviceId = servicesInCurrentAppointment.elementAt(index).serviceProviderServiceId ?? -1; + int serviceId = servicesInCurrentAppointment + .elementAt(index) + .serviceProviderServiceId ?? -1; allSelectedItemsInAppointments.removeWhere((element) => element.serviceProviderServiceId == serviceId); servicesInCurrentAppointment[index].serviceItems!.clear(); servicesInCurrentAppointment.removeAt(index); @@ -371,8 +386,8 @@ class AppointmentsVM extends BaseVM { if (shouldPopulateUpcoming) { myUpComingAppointments = myFilteredAppointmentsForProvider .where((element) => - (element.appointmentStatusEnum == AppointmentStatusEnum.booked || element.appointmentStatusEnum == AppointmentStatusEnum.confirmed) && - (DateHelper.parseStringToDate(element.appointmentDate!).isAfter(DateTime.now()))) + (element.appointmentStatusEnum == AppointmentStatusEnum.booked || element.appointmentStatusEnum == AppointmentStatusEnum.confirmed) && + (DateHelper.parseStringToDate(element.appointmentDate!).isAfter(DateTime.now()))) .toList(); log("myUpComingAppointments: ${myUpComingAppointments.length}"); } @@ -401,8 +416,8 @@ class AppointmentsVM extends BaseVM { if (shouldPopulateUpcoming) { myUpComingAppointments = myFilteredAppointmentsForCustomers .where((element) => - (element.appointmentStatusEnum == AppointmentStatusEnum.booked || element.appointmentStatusEnum == AppointmentStatusEnum.confirmed) && - (DateHelper.parseStringToDate(element.appointmentDate!).isAfter(DateTime.now()))) + (element.appointmentStatusEnum == AppointmentStatusEnum.booked || element.appointmentStatusEnum == AppointmentStatusEnum.confirmed) && + (DateHelper.parseStringToDate(element.appointmentDate!).isAfter(DateTime.now()))) .toList(); } @@ -417,9 +432,13 @@ class AppointmentsVM extends BaseVM { List groupedList = uniqueCustomerIDs.map((id) { List list = appointments.where((item) => item.customerID == id).toList(); - list.sort((a, b) => DateHelper.parseStringToDate(DateHelper.formatDateT(b.appointmentDate!)) - .millisecondsSinceEpoch - .compareTo(DateHelper.parseStringToDate(DateHelper.formatDateT(a.appointmentDate!)).millisecondsSinceEpoch)); + list.sort((a, b) => + DateHelper + .parseStringToDate(DateHelper.formatDateT(b.appointmentDate!)) + .millisecondsSinceEpoch + .compareTo(DateHelper + .parseStringToDate(DateHelper.formatDateT(a.appointmentDate!)) + .millisecondsSinceEpoch)); AppointmentListModel model = list.first; model.customerAppointmentList = list; return model; @@ -428,6 +447,35 @@ class AppointmentsVM extends BaseVM { return groupedList; } + /// Re-fetches upcoming appointments for the tapped appointment's own branch and + /// returns the index of its customer group in [myFilteredAppointmentsForProvider], + /// so the Home slider always opens the correct, up-to-date appointment detail + /// instead of relying on whatever branch/index was last used elsewhere. + Future refreshUpcomingAppointmentAndGetIndex(AppointmentListModel tappedAppointment) async { + int branchId = tappedAppointment.branchId ?? 0; + List freshAppointments = await getAppointmentsBasedOnFiltersForProviders( + branchID: branchId, + appointmentStatus: AppointmentStatusEnum.allAppointments, + ); + + List upcomingOnly = freshAppointments + .where((element) => + (element.appointmentStatusEnum == AppointmentStatusEnum.booked || element.appointmentStatusEnum == AppointmentStatusEnum.confirmed) && + (DateHelper.parseStringToDate(element.appointmentDate!).isAfter(DateTime.now()))) + .toList(); + + List groupedList = findAppointmentsBasedOnCustomers(upcomingOnly); + + int index = groupedList.indexWhere((element) => element.customerID == tappedAppointment.customerID); + if (index == -1) return null; + + myFilteredAppointmentsForProvider = groupedList; + selectedBranchIdForAppointments = branchId; + selectedAppointmentIndex = index; + notifyListeners(); + return index; + } + AppointmentSlots? appointmentSlots; bool isSlotsLoading = true; @@ -506,7 +554,7 @@ class AppointmentsVM extends BaseVM { void updateCheckBoxInMergeRequest(int currentIndex) { myFilteredAppointmentsForProvider[selectedAppointmentIndex].customerAppointmentList![currentIndex].isSelected = - !(myFilteredAppointmentsForProvider[selectedAppointmentIndex].customerAppointmentList?[currentIndex].isSelected ?? false); + !(myFilteredAppointmentsForProvider[selectedAppointmentIndex].customerAppointmentList?[currentIndex].isSelected ?? false); int count = countSelected(myFilteredAppointmentsForProvider[selectedAppointmentIndex].customerAppointmentList ?? []); if (count > 1) { @@ -518,7 +566,10 @@ class AppointmentsVM extends BaseVM { } int countSelected(List appointments) { - return appointments.where((appointment) => appointment.isSelected == true).toList().length; + return appointments + .where((appointment) => appointment.isSelected == true) + .toList() + .length; } updateSelectedAppointmentDate({required int dateIndex, required int scheduleIndex}) { @@ -563,7 +614,7 @@ class AppointmentsVM extends BaseVM { int serviceIndex = servicesInCurrentAppointment.indexWhere((element) => element.serviceId == currentServiceSelection!.serviceId!); serviceItemsFromApi[index].isUpdateOrSelected = selected; - serviceItemsFromApi[index].isHomeSelected = isHomeTapped; + serviceItemsFromApi[index].isHomeSelected = currentServiceSelection?.isHomeSelected ?? isHomeTapped; log(" serviceIndex: $serviceIndex"); if (selected) { @@ -709,13 +760,14 @@ class AppointmentsVM extends BaseVM { Column( children: List.generate( selectedService.serviceItems!.length, - (index) => Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - "${selectedService.serviceItems![index].name}".toText(fontSize: 12, color: MyColors.lightTextColor, isBold: true), - "${selectedService.serviceItems![index].price} ${LocaleKeys.sar.tr()}".toText(fontSize: 12, isBold: true), - ], - ), + (index) => + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + "${selectedService.serviceItems![index].name}".toText(fontSize: 12, color: MyColors.lightTextColor, isBold: true), + "${selectedService.serviceItems![index].price} ${LocaleKeys.sar.tr()}".toText(fontSize: 12, isBold: true), + ], + ), ), ), Row( @@ -934,7 +986,10 @@ class AppointmentsVM extends BaseVM { return InfoBottomSheet( title: LocaleKeys.reportComplaint.tr().toText(fontSize: 28, isBold: true, letterSpacing: -1.44), description: Padding( - padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), + padding: EdgeInsets.only(bottom: MediaQuery + .of(context) + .viewInsets + .bottom), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ diff --git a/lib/view_models/service_view_model.dart b/lib/view_models/service_view_model.dart index 836f739..1ed8fdb 100644 --- a/lib/view_models/service_view_model.dart +++ b/lib/view_models/service_view_model.dart @@ -651,8 +651,12 @@ class ServiceVM extends BaseVM { } } - Future cannotDeactivateServiceOrCategoryBottomSheet( - {required BuildContext mainContext, required List appointments, required String branchName, required bool isService}) async { + Future cannotDeactivateServiceOrCategoryBottomSheet({ + required BuildContext mainContext, + required List appointments, + required String branchName, + required bool isService, + }) async { return actionConfirmationBottomSheet( isOnlyOneButton: true, context: mainContext, @@ -666,21 +670,20 @@ class ServiceVM extends BaseVM { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + // "${appointmentBasicDetailsModel.customerID}".toText(fontSize: 16, letterSpacing: -0.64), + showItem("${LocaleKeys.branchName.tr()}:", branchName), if (appointmentBasicDetailsModel.appointmentStatusText != null && appointmentBasicDetailsModel.appointmentStatusText!.isNotEmpty) ...[ - Utils.statusContainerChip(text: appointmentBasicDetailsModel.appointmentStatusText!, chipColor: MyColors.black), + showItem("${LocaleKeys.appointmentStatusLabel.tr()}:", appointmentBasicDetailsModel.appointmentStatusText!), ], - 6.height, - "${appointmentBasicDetailsModel.customerID}".toText(fontSize: 16, letterSpacing: -0.64), - showItem("${LocaleKeys.branchName.tr()}:", "${appointmentBasicDetailsModel.slotDate}"), showItem("${LocaleKeys.date.tr()}:", "${appointmentBasicDetailsModel.slotDate}"), - showItem("${LocaleKeys.time.tr()}:", "${appointmentBasicDetailsModel.startTime} - ${appointmentBasicDetailsModel.startTime}"), + showItem("${LocaleKeys.time.tr()}:", "${appointmentBasicDetailsModel.startTime} - ${appointmentBasicDetailsModel.endTime}"), ], ).toContainer(isShadowEnabled: true); }), actionButtonYes: Expanded( child: ShowFillButton( maxHeight: 55, - title: LocaleKeys.done.tr(), + title: LocaleKeys.ok.tr(), fontSize: 15, onPressed: () => Navigator.pop(mainContext), ), @@ -688,7 +691,7 @@ class ServiceVM extends BaseVM { actionButtonNo: Expanded( child: ShowFillButton( maxHeight: 55, - title: LocaleKeys.done.tr(), + title: LocaleKeys.ok.tr(), fontSize: 15, onPressed: () => Navigator.pop(mainContext), ), diff --git a/lib/view_models/subscriptions_view_model.dart b/lib/view_models/subscriptions_view_model.dart index 642492b..73e88f2 100644 --- a/lib/view_models/subscriptions_view_model.dart +++ b/lib/view_models/subscriptions_view_model.dart @@ -148,11 +148,18 @@ class SubscriptionsVM extends BaseVM { } String _convertDaysToMonths(int days) { - final int months = days ~/ 30; - final int remainingDays = days % 30; + const int daysPerMonth = 30; + const int roundingToleranceDays = 6; // absorbs real calendar month/year variance vs. the flat 30-day assumption (e.g. 365 vs 360 for "12 Months") + + final int months = days ~/ daysPerMonth; + int remainingDays = days % daysPerMonth; + if (months > 0 && remainingDays <= roundingToleranceDays) { + remainingDays = 0; + } String result = months > 0 ? '$months Month${months > 1 ? 's' : ''}${remainingDays > 0 ? ' & ' : ''}' : ''; result += remainingDays > 0 ? '$remainingDays Day${remainingDays > 1 ? 's' : ''}' : ''; + if (result.isEmpty) result = '$days Day${days > 1 ? 's' : ''}'; return result; } diff --git a/lib/views/advertisement/ads_detail_view/ads_detail_view.dart b/lib/views/advertisement/ads_detail_view/ads_detail_view.dart index 0fe45b6..d6c9d0e 100644 --- a/lib/views/advertisement/ads_detail_view/ads_detail_view.dart +++ b/lib/views/advertisement/ads_detail_view/ads_detail_view.dart @@ -1,6 +1,5 @@ import 'dart:async'; import 'dart:developer'; -import 'dart:io'; import 'package:flutter/material.dart'; import 'package:mc_common_app/classes/app_state.dart'; import 'package:mc_common_app/classes/consts.dart'; @@ -18,6 +17,7 @@ import 'package:mc_common_app/view_models/ad_view_model.dart'; import 'package:mc_common_app/view_models/payment_view_model.dart'; import 'package:mc_common_app/views/advertisement/ads_detail_view/components.dart'; import 'package:mc_common_app/views/advertisement/bottom_sheets/ad_duration_selection_sheet.dart'; +import 'package:mc_common_app/views/advertisement/bottom_sheets/ad_qr_code_sheet.dart'; import 'package:mc_common_app/views/advertisement/bottom_sheets/ads_damage_part_pictures_sheet.dart'; import 'package:mc_common_app/views/advertisement/components/ads_images_corousel_widget.dart'; import 'package:mc_common_app/widgets/bottom_sheet.dart'; @@ -106,8 +106,10 @@ class _AdsDetailViewState extends State { child: Row( children: [ ShareAdButton(adDetails: widget.adDetails), - 8.width, - FavoriteAdButton(adDetails: widget.adDetails), + if (!(widget.adDetails.isMyAd ?? false) && widget.adDetails.adPostStatus == AdPostStatus.active) ...[ + 8.width, + FavoriteAdButton(adDetails: widget.adDetails), + ], ], ), ), @@ -352,6 +354,25 @@ class _AdsDetailViewState extends State { }).toWhiteContainer(width: double.infinity, allPading: 12); } + Widget buildQrCodeSection() { + return Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + const Icon(Icons.qr_code_2, color: MyColors.darkTextColor), + 10.width, + LocaleKeys.qrCode.tr().toText(fontSize: 14), + ], + ), + const Icon(Icons.arrow_forward_ios_rounded, size: 20) + ], + ).paddingOnly(top: 5, bottom: 5).onPress(() { + showMyBottomSheet(context, child: AdQrCodeSheet(adDetails: widget.adDetails)); + }).toWhiteContainer(width: double.infinity, allPading: 12); + } + Future onExtendAdPressed() async { final AdVM adVM = context.read(); return actionConfirmationBottomSheet( @@ -535,6 +556,10 @@ class _AdsDetailViewState extends State { if (widget.adDetails.vehicle!.damagereport != null && widget.adDetails.vehicle!.damagereport!.isNotEmpty) ...[ buildDamagePartDetails(), ], + if ((widget.adDetails.qrCodePath ?? "").isNotEmpty) ...[ + 12.height, + buildQrCodeSection(), + ], if ((!(widget.adDetails.isMyAd ?? false) && (widget.adDetails.showContactDetail ?? false))) ...[ 12.height, buildPersonalInformationCard(context: context), @@ -714,18 +739,9 @@ class ShareAdButton extends StatelessWidget { } shareMessage += '\n'; - // App download links - shareMessage += '${LocaleKeys.downloadApp.tr()}\n'; - - if (Platform.isAndroid) { - shareMessage += '${LocaleKeys.androidApp.tr()}: https://play.google.com/store/apps/details?id=com.mowater.app\n'; - } else if (Platform.isIOS) { - shareMessage += '${LocaleKeys.iosApp.tr()}: https://apps.apple.com/app/mowater/id123456789\n'; - } else { - // Include both if platform detection fails - shareMessage += '${LocaleKeys.androidApp.tr()}: https://play.google.com/store/apps/details?id=com.mowater.app\n'; - shareMessage += '${LocaleKeys.iosApp.tr()}: https://apps.apple.com/app/mowater/id123456789\n'; - } + // Dynamic ad link - opens directly in the app if installed, otherwise + // falls back to the store via the hosted web page for this path. + shareMessage += 'https://ms.hmg.com/ad/${adDetails.id}\n'; // Share using share_plus Share.share(shareMessage); diff --git a/lib/views/advertisement/ads_detail_view/components.dart b/lib/views/advertisement/ads_detail_view/components.dart index cfa86d0..2933785 100644 --- a/lib/views/advertisement/ads_detail_view/components.dart +++ b/lib/views/advertisement/ads_detail_view/components.dart @@ -109,7 +109,7 @@ class BuildAdDetailsActionButtonForExploreAds extends StatelessWidget { ], ), const Divider(), - LocaleKeys.specialService.toText(fontSize: 16, isBold: true), + LocaleKeys.specialService.tr().toText(fontSize: 16, isBold: true), 5.height, Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -245,8 +245,8 @@ class BuildAdDetailsActionButtonForExploreAds extends StatelessWidget { } Widget defaultActionForProviderAndCustomer(BuildContext context, AdDetailsModel adDetailsModel) { - return (adDetailsModel.phoneNo != null && adDetailsModel.phoneNo!.isNotEmpty) - ? Row( + return (adDetailsModel.showContactDetail ?? false) + ? Row( children: [ Expanded( child: ShowFillButton( diff --git a/lib/views/advertisement/ads_filter_view.dart b/lib/views/advertisement/ads_filter_view.dart index b11171f..b3d4bbf 100644 --- a/lib/views/advertisement/ads_filter_view.dart +++ b/lib/views/advertisement/ads_filter_view.dart @@ -190,6 +190,28 @@ class _AdsFilterViewState extends State { onHistoryItemDeleted: (index) => adVM.removeVehicleBrandsAdSearchHistory(index: index), onHistoryItemTapped: (DropValue value) => null, ), + if (adVM.vehicleBrandsAdSearchHistory.isNotEmpty) ...[ + const Divider(thickness: 1.2).paddingOnly(top: 7, bottom: 7), + SearchEntityWidget( + title: LocaleKeys.searchByVehicleModel.tr(), + actionWidget: Builder(builder: (context) { + List vehicleModelsDrop = []; + for (var element in adVM.vehicleModelsForSelectedBrandsFilter) { + vehicleModelsDrop.add(DropValue(element.id?.toInt() ?? 0, element.model ?? "", "")); + } + return DropdownField( + (DropValue value) => adVM.updateSelectionVehicleModelId(SelectionModel(selectedId: value.id, selectedOption: value.value), isForSearch: true), + list: vehicleModelsDrop, + dropdownValue: adVM.vehicleModelId.selectedId != -1 ? DropValue(adVM.vehicleModelId.selectedId, adVM.vehicleModelId.selectedOption, "") : null, + hint: LocaleKeys.selectVehicleModel.tr(), + errorValue: adVM.vehicleModelId.errorValue, + ); + }), + historyContent: adVM.vehicleModelsAdSearchHistory, + onHistoryItemDeleted: (index) => adVM.removeVehicleModelsAdSearchHistory(index: index), + onHistoryItemTapped: (DropValue value) => null, + ), + ], const Divider(thickness: 1.2).paddingOnly(top: 7, bottom: 7), SearchEntityWidget( title: LocaleKeys.searchByVehicleYear.tr(), diff --git a/lib/views/advertisement/bottom_sheets/ad_qr_code_sheet.dart b/lib/views/advertisement/bottom_sheets/ad_qr_code_sheet.dart new file mode 100644 index 0000000..3bc8a48 --- /dev/null +++ b/lib/views/advertisement/bottom_sheets/ad_qr_code_sheet.dart @@ -0,0 +1,46 @@ +import 'dart:developer'; + +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:mc_common_app/extensions/int_extensions.dart'; +import 'package:mc_common_app/extensions/string_extensions.dart'; +import 'package:mc_common_app/generated/locale_keys.g.dart'; +import 'package:mc_common_app/models/advertisment_models/ad_details_model.dart'; +import 'package:mc_common_app/widgets/extensions/extensions_widget.dart'; + +class AdQrCodeSheet extends StatelessWidget { + final AdDetailsModel adDetails; + + const AdQrCodeSheet({super.key, required this.adDetails}); + + @override + Widget build(BuildContext context) { + log("dDetails.qrCodePath: ${adDetails.qrCodePath}"); + return SizedBox( + width: double.infinity, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + LocaleKeys.qrCode.tr().toText(fontSize: 24, isBold: true), + ], + ), + 20.height, + SizedBox( + height: 220, + width: 220, + child: CachedNetworkImage( + imageUrl: "${adDetails.qrCodePath}", + placeholder: (context, url) => const Center(child: CircularProgressIndicator()), + errorWidget: (context, url, error) => const Icon(Icons.error), + fit: BoxFit.contain, + ), + ), + 20.height, + ], + ), + ).horPaddingMain(); + } +} diff --git a/lib/views/advertisement/components/ads_list_widget.dart b/lib/views/advertisement/components/ads_list_widget.dart index de125af..e77c7bc 100644 --- a/lib/views/advertisement/components/ads_list_widget.dart +++ b/lib/views/advertisement/components/ads_list_widget.dart @@ -28,6 +28,7 @@ class AdsListWidget extends StatelessWidget { final bool hasMoreData; final bool isDraftAds; final Function()? onFetchMoreAds; + final Function()? onReturnFromAdDetail; const AdsListWidget({ super.key, @@ -36,6 +37,7 @@ class AdsListWidget extends StatelessWidget { required this.hasMoreData, required this.isDraftAds, this.onFetchMoreAds, + this.onReturnFromAdDetail, this.scrollPhysics, this.isAdsFragment = false, }); @@ -97,7 +99,9 @@ class AdsListWidget extends StatelessWidget { isForDraft: true, ); } else { - navigateWithName(context, AppRoutes.adsDetailView, arguments: adsList[index]); + Navigator.pushNamed(context, AppRoutes.adsDetailView, arguments: adsList[index]).then((_) { + onReturnFromAdDetail?.call(); + }); } }, ).toViewOnly(context, onTap: () => navigateWithName(context, AppRoutes.loginWithPassword, arguments: false)); diff --git a/lib/views/advertisement/favourite_ads_view.dart b/lib/views/advertisement/favourite_ads_view.dart index 54a3968..ca938b6 100644 --- a/lib/views/advertisement/favourite_ads_view.dart +++ b/lib/views/advertisement/favourite_ads_view.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'package:mc_common_app/generated/locale_keys.g.dart'; -import 'package:mc_common_app/models/advertisment_models/ad_details_model.dart'; import 'package:mc_common_app/view_models/ad_view_model.dart'; import 'package:flutter/material.dart'; import 'package:mc_common_app/extensions/int_extensions.dart'; @@ -28,11 +27,19 @@ class _FavouriteAdsViewState extends State { void initState() { scheduleMicrotask(() async { final adVM = context.read(); - await adVM.getFavouriteAds(); // Load filter options if not already loaded if (adVM.exploreAdsFilterOptions.isEmpty) { await adVM.populateAdsFilterList(); } + // exploreAdsFilterOptions is shared with the Explore Ads screen, so its selection + // may be left on a different brand from there — reset to "All" for this screen. + for (var filter in adVM.exploreAdsFilterOptions) { + filter.isSelected = filter.id == 0; + } + setState(() { + selectedBrandId = 0; + }); + await adVM.getFavouriteAds(); }); super.initState(); } @@ -69,7 +76,7 @@ class _FavouriteAdsViewState extends State { filter.isSelected = false; } adVM.exploreAdsFilterOptions[index].isSelected = true; - + // Call API with filter (0 = all brands) await adVM.getFavouriteAds( vehicleBrandId: selectedFilterId == 0 ? null : selectedFilterId, @@ -99,8 +106,12 @@ class _FavouriteAdsViewState extends State { isAdsFragment: false, isDraftAds: false, shouldShowAdStatus: false, - adsList: adVM.favouriteAds, // Use API-filtered list directly + adsList: adVM.favouriteAds, + // Use API-filtered list directly hasMoreData: false, + onReturnFromAdDetail: () => adVM.getFavouriteAds( + vehicleBrandId: selectedBrandId == 0 ? null : selectedBrandId, + ), ), ), ), diff --git a/lib/views/appointments/book_appointment_services_view.dart b/lib/views/appointments/book_appointment_services_view.dart index cd1940e..097c42b 100644 --- a/lib/views/appointments/book_appointment_services_view.dart +++ b/lib/views/appointments/book_appointment_services_view.dart @@ -5,7 +5,6 @@ import 'package:mc_common_app/config/routes.dart'; import 'package:mc_common_app/extensions/int_extensions.dart'; import 'package:mc_common_app/extensions/string_extensions.dart'; import 'package:mc_common_app/generated/locale_keys.g.dart'; -import 'package:mc_common_app/models/general_models/widgets_models.dart'; import 'package:mc_common_app/models/services_models/item_model.dart'; import 'package:mc_common_app/models/services_models/service_model.dart'; import 'package:mc_common_app/theme/colors.dart'; @@ -64,13 +63,7 @@ class BookAppointmentServicesView extends StatelessWidget { Row( children: [ MyAssets.icEdit.buildSvg(height: 17, width: 17).onPress(() { - appointmentsVM.updateBranchServiceId( - SelectionModel( - selectedId: serviceData.serviceProviderServiceId!, - selectedOption: serviceData.serviceDescription ?? "", - itemPrice: "", - ), - ); + appointmentsVM.editServiceInCurrentAppointment(serviceData); appointmentsVM.getServiceItems(serviceId: serviceData.serviceProviderServiceId!); navigateWithName(context, AppRoutes.bookAppointmentsItemView); }), @@ -110,8 +103,7 @@ class BookAppointmentServicesView extends StatelessWidget { Builder(builder: (context) { double totalKms = double.parse(serviceData.servicelocationInfo.distanceToBranch.toStringAsFixed(2)); double pricePerKm = double.parse(serviceData.servicelocationInfo.homeChargesInCurrentService.toStringAsFixed(2)); - return ((serviceData.currentTotalServicePrice + (pricePerKm * totalKms)).toStringAsFixed(2)) - .toText(fontSize: 32, isBold: true); + return ((serviceData.currentTotalServicePrice + (pricePerKm * totalKms)).toStringAsFixed(2)).toText(fontSize: 32, isBold: true); }), ] else ...[ ((serviceData.currentTotalServicePrice).toString()).toText(fontSize: 32, isBold: true), @@ -124,8 +116,8 @@ class BookAppointmentServicesView extends StatelessWidget { ], ], ).toWhiteContainer( - width: double.infinity, - allPading: 12, + width: double.infinity, + allPading: 12, margin: const EdgeInsets.symmetric(horizontal: 21, vertical: 10), onTap: () => appointmentsVM.priceBreakDownClicked(context, serviceData), ); diff --git a/lib/views/appointments/widgets/common_appointment_slider_widget.dart b/lib/views/appointments/widgets/common_appointment_slider_widget.dart index 4ed8f6a..b0c8031 100644 --- a/lib/views/appointments/widgets/common_appointment_slider_widget.dart +++ b/lib/views/appointments/widgets/common_appointment_slider_widget.dart @@ -1,3 +1,5 @@ +import 'dart:developer'; + import 'package:carousel_slider/carousel_slider.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; @@ -168,7 +170,8 @@ class AppointmentHomeTileWidget extends StatelessWidget { height: 19 / 12, ) : const SizedBox(), - (AppState().currentAppType == AppType.provider ? appointmentListModel!.customerName ?? "" : appointmentListModel!.branchName ?? "").toText(color: MyColors.black, fontSize: 16, letterSpacing: -0.64, height: 24 / 16), + (AppState().currentAppType == AppType.provider ? appointmentListModel!.customerName ?? "" : appointmentListModel!.branchName ?? "") + .toText(color: MyColors.black, fontSize: 16, letterSpacing: -0.64, height: 24 / 16), Row( children: [ !isForProvider ? MyAssets.miniClock.buildSvg(height: 12) : const SizedBox(), @@ -296,11 +299,21 @@ class AppointmentFragmentTileWidget extends StatelessWidget { (appointmentListModel!.branchName ?? "").toText(color: MyColors.darkTextColor, fontSize: 12, letterSpacing: -0.48, height: 18 / 12), ], ), + if (appointmentListModel!.appointmentType != null) + Row( + children: [ + ("${LocaleKeys.serviceLocation.tr()}:").toText(color: MyColors.greyColor, fontSize: 12, letterSpacing: -0.48, height: 18 / 12), + 2.width, + (appointmentListModel!.appointmentType == 1 ? LocaleKeys.companyLocation.tr() : LocaleKeys.customerLocation.tr()) + .toText(color: MyColors.darkTextColor, fontSize: 12, letterSpacing: -0.48, height: 18 / 12), + ], + ), Row( children: [ MyAssets.miniClock.buildSvg(height: 12), 2.width, - " ${appointmentListModel!.duration ?? ""} ${appointmentListModel!.appointmentDate!.toFormattedDateWithoutTime()}".toText(color: MyColors.lightTextColor, fontSize: 12, letterSpacing: -0.48, height: 19 / 12), + " ${appointmentListModel!.duration ?? ""} ${appointmentListModel!.appointmentDate!.toFormattedDateWithoutTime()}" + .toText(color: MyColors.lightTextColor, fontSize: 12, letterSpacing: -0.48, height: 19 / 12), ], ), 5.height, diff --git a/lib/views/common_fragments/ads_fragment.dart b/lib/views/common_fragments/ads_fragment.dart index b2a00c3..e0d7b08 100644 --- a/lib/views/common_fragments/ads_fragment.dart +++ b/lib/views/common_fragments/ads_fragment.dart @@ -38,6 +38,7 @@ class AdsFragment extends StatelessWidget { @override Widget build(BuildContext context) { + final isArabic = AppState().isArabic(context); return Consumer( builder: (BuildContext context, AdVM adVM, Widget? child) { return Scaffold( @@ -46,8 +47,7 @@ class AdsFragment extends StatelessWidget { isRemoveBackButton: true, actions: [ Padding( - padding: EdgeInsets.only( - top: adVM.adsFiltersCounter > 0 ? 20 : 0, right: 21), + padding: EdgeInsets.only(top: adVM.adsFiltersCounter > 0 ? 20 : 0, right: isArabic ? 0 : 16, left: isArabic ? 16 : 0), child: Badge( isLabelVisible: adVM.adsFiltersCounter > 0, largeSize: 15, @@ -61,8 +61,7 @@ class AdsFragment extends StatelessWidget { }) ], ).toViewOnly(context, onTap: () { - navigateWithName(context, AppRoutes.loginWithPassword, - arguments: false); + navigateWithName(context, AppRoutes.loginWithPassword, arguments: false); }), body: Stack( children: [ @@ -73,8 +72,7 @@ class AdsFragment extends StatelessWidget { children: [ 16.height, Consumer( - builder: - (BuildContext context, AdVM adVM, Widget? child) { + builder: (BuildContext context, AdVM adVM, Widget? child) { return Column( children: [ Row( @@ -85,9 +83,7 @@ class AdsFragment extends StatelessWidget { maxHeight: 55, fontSize: 15, title: LocaleKeys.exploreAds.tr(), - txtColor: adVM.isExploreAdsTapped - ? MyColors.white - : MyColors.darkTextColor, + txtColor: adVM.isExploreAdsTapped ? MyColors.white : MyColors.darkTextColor, onPressed: () { log("accessToken: ${AppState().getUser.data!.accessToken}"); log("AppState().getUser.data!.userInfo!.userId;: ${AppState().getUser.data!.userInfo!.userId}"); @@ -103,9 +99,7 @@ class AdsFragment extends StatelessWidget { Expanded( child: ShowFillButton( isFilled: !adVM.isExploreAdsTapped, - txtColor: !adVM.isExploreAdsTapped - ? MyColors.white - : MyColors.darkTextColor, + txtColor: !adVM.isExploreAdsTapped ? MyColors.white : MyColors.darkTextColor, maxHeight: 55, fontSize: 15, title: LocaleKeys.myAds.tr(), @@ -113,9 +107,7 @@ class AdsFragment extends StatelessWidget { adVM.updateIsExploreAds(false); }, ).toViewOnly(context, onTap: () { - navigateWithName( - context, AppRoutes.loginWithPassword, - arguments: false); + navigateWithName(context, AppRoutes.loginWithPassword, arguments: false); }), ), ], @@ -127,8 +119,7 @@ class AdsFragment extends StatelessWidget { showImages: true, filterList: adVM.exploreAdsFilterOptions, onFilterTapped: (index, selectedFilterId) { - adVM.applyFilterOnExploreAds( - vehicleBrandId: selectedFilterId); + adVM.applyFilterOnExploreAds(vehicleBrandId: selectedFilterId); }, needLeftPadding: false, ).paddingOnly(left: 21), @@ -138,9 +129,7 @@ class AdsFragment extends StatelessWidget { FiltersList( filterList: adVM.myAdsFilterOptions, onFilterTapped: (index, selectedFilterId) { - adVM.applyFilterOnMyAds( - adPostStatusEnum: - selectedFilterId.toAdPostEnum()); + adVM.applyFilterOnMyAds(adPostStatusEnum: selectedFilterId.toAdPostEnum()); }, needLeftPadding: false, ).paddingOnly(left: 21), @@ -154,19 +143,11 @@ class AdsFragment extends StatelessWidget { child: RefreshIndicator( onRefresh: () async { if (adVM.isExploreAdsTapped) { - int vehicleBrandId = adVM.exploreAdsFilterOptions - .firstWhere((element) => element.isSelected) - .id; - adVM.applyFilterOnExploreAds( - vehicleBrandId: vehicleBrandId); + 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); + AdPostStatus adPostStatusEnum = adVM.myAdsFilterOptions.firstWhere((element) => element.isSelected).id.toAdPostEnum(); + adVM.applyFilterOnMyAds(adPostStatusEnum: adPostStatusEnum); } }, child: adVM.state == ViewState.busy @@ -176,22 +157,14 @@ class AdsFragment extends StatelessWidget { isDraftAds: false, shouldShowAdStatus: !adVM.isExploreAdsTapped, adsList: getAdsList(adVM), - hasMoreData: adVM.isExploreAdsTapped - ? adVM.hasMoreDataForExploreAds - : adVM.hasMoreDataForMyAds, + hasMoreData: adVM.isExploreAdsTapped ? adVM.hasMoreDataForExploreAds : adVM.hasMoreDataForMyAds, onFetchMoreAds: () async { - AdPostStatus adPostStatusEnum = adVM - .myAdsFilterOptions - .firstWhere( - (element) => element.isSelected) - .id - .toAdPostEnum(); + AdPostStatus adPostStatusEnum = adVM.myAdsFilterOptions.firstWhere((element) => element.isSelected).id.toAdPostEnum(); if (adVM.isExploreAdsTapped) { await adVM.fetchMoreExploreAds(); } else { - await adVM.fetchMoreMyAds( - adsStatus: adPostStatusEnum); + await adVM.fetchMoreMyAds(adsStatus: adPostStatusEnum); } }, ), @@ -215,8 +188,7 @@ class AdsFragment extends StatelessWidget { color: Colors.black.withOpacity(0.3), child: const Center( child: CircularProgressIndicator( - valueColor: AlwaysStoppedAnimation( - MyColors.darkPrimaryColor), + valueColor: AlwaysStoppedAnimation(MyColors.darkPrimaryColor), ), ), ), @@ -233,31 +205,23 @@ class AdsFragment extends StatelessWidget { } if (AppState().userType != UserType.providerDealer) { - if (adVM.vehicleAdDurationId.selectedId == -1 && - adVM.vehicleAdsDurations.isNotEmpty) { + if (adVM.vehicleAdDurationId.selectedId == -1 && adVM.vehicleAdsDurations.isNotEmpty) { SelectionModel selection = SelectionModel( selectedId: adVM.vehicleAdsDurations.first.id ?? 0, - selectedOption: - ("${adVM.vehicleAdsDurations.first.days} ${LocaleKeys.daysVar.tr()}"), + selectedOption: ("${adVM.vehicleAdsDurations.first.days} ${LocaleKeys.daysVar.tr()}"), itemPrice: adVM.vehicleAdsDurations.first.price!.toString(), - currency: - adVM.vehicleAdsDurations.first.currency!.toString(), + currency: adVM.vehicleAdsDurations.first.currency!.toString(), ); adVM.updateVehicleAdDurationId(selection); } } - navigateWithName(context, AppRoutes.selectAdTypeView, arguments: [ - injector.get().currentAppType == AppType.provider, - false, - -1 - ]); + navigateWithName(context, AppRoutes.selectAdTypeView, arguments: [injector.get().currentAppType == AppType.provider, false, -1]); }, backgroundColor: MyColors.darkPrimaryColor, child: const Icon(Icons.add, color: MyColors.white), ).toViewOnly(context, onTap: () { - navigateWithName(context, AppRoutes.loginWithPassword, - arguments: false); + navigateWithName(context, AppRoutes.loginWithPassword, arguments: false); }), ); }, diff --git a/lib/views/user/update_user_details.dart b/lib/views/user/update_user_details.dart index 1a51d29..1088be8 100644 --- a/lib/views/user/update_user_details.dart +++ b/lib/views/user/update_user_details.dart @@ -26,6 +26,8 @@ class _UpdateUserDetailsState extends State { @override void initState() { userVM = Provider.of(context, listen: false); + firstname = AppState().getUser.data?.userInfo?.firstName ?? ""; + lastname = AppState().getUser.data?.userInfo?.lastName ?? ""; super.initState(); } @@ -44,11 +46,13 @@ class _UpdateUserDetailsState extends State { children: [ TxtField( hint: LocaleKeys.enterNewFirstName.tr(), + value: firstname, onChanged: (v) => firstname = v, ), 12.height, TxtField( hint: LocaleKeys.enterNewLastName.tr(), + value: lastname, onChanged: (v) => lastname = v, ), 40.height, diff --git a/lib/widgets/common_widgets/app_bar.dart b/lib/widgets/common_widgets/app_bar.dart index aa9314f..064cb6a 100644 --- a/lib/widgets/common_widgets/app_bar.dart +++ b/lib/widgets/common_widgets/app_bar.dart @@ -48,6 +48,7 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget { @override Widget build(BuildContext context) { + final isArabic = AppState().isArabic(context); return Column( children: [ AppBar( @@ -101,7 +102,7 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget { 10.width, SvgPicture.asset(MyAssets.dashboardDrawerIcon), ], - ).paddingOnly(left: 21), + ).paddingOnly(left: isArabic ? 0 : 21, right: isArabic ? 21 : 0), ) : isRemoveBackButton ? null @@ -110,7 +111,7 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget { 21.width, const Icon(Icons.arrow_back_ios, color: Colors.black, size: 16) .toContainer( - padding: const EdgeInsets.only(left: 5), + padding: EdgeInsets.only(left: isArabic ? 0 : 5, right: isArabic ? 5 : 0), borderRadius: 100, borderColor: MyColors.lightGreyEFColor, isEnabledBorder: true, diff --git a/lib/widgets/dropdown/dropdow_field.dart b/lib/widgets/dropdown/dropdow_field.dart index 6d3ba97..bba74b0 100644 --- a/lib/widgets/dropdown/dropdow_field.dart +++ b/lib/widgets/dropdown/dropdow_field.dart @@ -1,8 +1,10 @@ +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:mc_common_app/classes/consts.dart'; import 'package:mc_common_app/extensions/int_extensions.dart'; import 'package:mc_common_app/extensions/string_extensions.dart'; +import 'package:mc_common_app/generated/locale_keys.g.dart'; import 'package:mc_common_app/theme/colors.dart'; import 'package:mc_common_app/utils/utils.dart'; import 'package:mc_common_app/widgets/extensions/extensions_widget.dart'; @@ -89,45 +91,47 @@ class _DropdownFieldState extends State { ) else // Show normal dropdown when selectable - Container( - 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, - child: DropdownButton( - value: dropdownValue, - icon: const Icon( - Icons.keyboard_arrow_down_sharp, - size: 21, - ), - elevation: 16, - iconSize: widget.showAppointmentPickerVariant ? 26 : 16, - iconEnabledColor: borderColor, - iconDisabledColor: borderColor, - isExpanded: true, - style: const TextStyle( - color: borderColor, - fontSize: 15, + Builder(builder: (context) { + // Distinguish "no list passed" (use placeholder defaultV) from "list explicitly empty" + // (real data fetched but nothing available) so we can show a meaningful empty-state. + final List items = widget.list ?? defaultV; + final bool hasNoData = widget.list != null && widget.list!.isEmpty; + return InkWell( + onTap: hasNoData + ? null + : () { + widget.onTap?.call(); + _openSearchablePicker(context, items); + }, + child: Container( + 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, top: 2, bottom: 2), + width: widget.showAppointmentPickerVariant ? 170 : null, + child: Row( + mainAxisSize: widget.showAppointmentPickerVariant ? MainAxisSize.min : MainAxisSize.max, + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: (dropdownValue?.value ?? (hasNoData ? LocaleKeys.noDataAvailable.tr() : "")).toText( + fontSize: 15, + color: hasNoData ? MyColors.greyACColor : MyColors.darkTextColor, + fontWeight: MyFonts.Medium, + maxLines: 1, + ), + ), + ), + Icon( + Icons.keyboard_arrow_down_sharp, + size: widget.showAppointmentPickerVariant ? 26 : 16, + color: hasNoData ? MyColors.greyACColor : MyColors.darkTextColor, + ), + ], + ), ), - underline: Container(height: 0), - onChanged: (DropValue? newValue) { - setState(() { - dropdownValue = newValue!; - widget.onSelect(newValue); - }); - }, - onTap: widget.onTap, - items: (widget.list ?? defaultV).map>( - (DropValue value) { - return DropdownMenuItem( - value: value, - enabled: value.isEnabled ?? true, - child: value.value.toText(fontSize: 15, color: value.isEnabled == false ? MyColors.darkTextColor : null, fontWeight: MyFonts.Medium), - ); - }, - ).toList(), - ), - ), + ); + }), if (widget.errorValue != "") Row( mainAxisAlignment: MainAxisAlignment.end, @@ -138,4 +142,166 @@ class _DropdownFieldState extends State { ], ); } + + void _openSearchablePicker(BuildContext context, List items) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (ctx) { + return _SearchableDropdownSheet( + items: items, + selectedValue: dropdownValue, + onSelected: (value) { + setState(() { + dropdownValue = value; + }); + widget.onSelect(value); + }, + ); + }, + ); + } +} + +class _SearchableDropdownSheet extends StatefulWidget { + final List items; + final DropValue? selectedValue; + final Function(DropValue) onSelected; + + const _SearchableDropdownSheet({required this.items, required this.selectedValue, required this.onSelected}); + + @override + State<_SearchableDropdownSheet> createState() => _SearchableDropdownSheetState(); +} + +class _SearchableDropdownSheetState extends State<_SearchableDropdownSheet> { + final TextEditingController _searchController = TextEditingController(); + String _searchQuery = ''; + + @override + void dispose() { + _searchController.dispose(); + super.dispose(); + } + + List get _filteredItems { + if (_searchQuery.isEmpty) return widget.items; + return widget.items.where((element) => element.value.toLowerCase().contains(_searchQuery.toLowerCase())).toList(); + } + + Widget _buildSearchField() { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Container( + height: 55, + decoration: BoxDecoration( + color: Colors.grey[200], + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.grey[300]!, width: 1), + ), + child: TextField( + controller: _searchController, + autofocus: false, + style: const TextStyle(fontSize: 16), + onChanged: (value) { + setState(() { + _searchQuery = value; + }); + }, + decoration: InputDecoration( + hintStyle: TextStyle(fontSize: 16, color: Colors.grey[600]), + prefixIcon: Icon(Icons.search, size: 26, color: Colors.grey[700]), + suffixIcon: _searchQuery.isEmpty + ? null + : InkWell( + onTap: () { + setState(() { + _searchController.clear(); + _searchQuery = ''; + }); + }, + child: Icon(Icons.close, size: 22, color: Colors.grey[700]), + ), + border: InputBorder.none, + contentPadding: const EdgeInsets.symmetric(horizontal: 18, vertical: 16), + ), + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + final filtered = _filteredItems; + return Padding( + padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), + child: Container( + decoration: const BoxDecoration( + color: MyColors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(18)), + ), + constraints: BoxConstraints(maxHeight: MediaQuery.of(context).size.height * 0.7), + child: SafeArea( + top: false, + // Material ancestor so ListTile ink splashes render on top of the white background above, + // instead of being hidden behind it. + child: Material( + type: MaterialType.transparency, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + 12.height, + Container( + width: 40, + height: 4, + decoration: BoxDecoration(color: Colors.grey[300], borderRadius: BorderRadius.circular(2)), + ), + 14.height, + _buildSearchField(), + 10.height, + Flexible( + child: filtered.isEmpty + ? Padding( + padding: const EdgeInsets.symmetric(vertical: 30), + child: LocaleKeys.noDataAvailable.tr().toText(fontSize: 15, color: MyColors.greyACColor), + ) + : ListView.separated( + shrinkWrap: true, + padding: const EdgeInsets.symmetric(vertical: 4), + itemCount: filtered.length, + separatorBuilder: (context, index) => Divider(height: 1, thickness: 1, color: Colors.grey[300]), + itemBuilder: (context, index) { + DropValue value = filtered[index]; + bool isEnabled = value.isEnabled ?? true; + bool isSelected = widget.selectedValue?.id == value.id; + return ListTile( + enabled: isEnabled, + selected: isSelected, + dense: false, + selectedTileColor: MyColors.darkPrimaryColor.withValues(alpha: 0.08), + title: value.value.toText( + fontSize: 15, + color: !isEnabled ? MyColors.greyACColor : (isSelected ? MyColors.darkPrimaryColor : MyColors.darkTextColor), + fontWeight: isSelected ? MyFonts.Bold : MyFonts.Medium, + ), + trailing: isSelected ? const Icon(Icons.check, color: MyColors.darkPrimaryColor) : null, + onTap: !isEnabled + ? null + : () { + widget.onSelected(value); + Navigator.pop(context); + }, + ); + }, + ), + ), + 8.height, + ], + ), + ), + ), + ), + ); + } } diff --git a/pubspec.lock b/pubspec.lock index 0f627d3..d0f41f3 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -9,6 +9,38 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.41" + app_links: + dependency: "direct main" + description: + name: app_links + sha256: f8db46d2ea9ff6f3a37191a7fd5b7813da1253ace8c32c1b9eadace41d1188ea + url: "https://pub.dev" + source: hosted + version: "7.2.1" + app_links_linux: + dependency: transitive + description: + name: app_links_linux + sha256: f5f7173a78609f3dfd4c2ff2c95bd559ab43c80a87dc6a095921d96c05688c81 + url: "https://pub.dev" + source: hosted + version: "1.0.3" + app_links_platform_interface: + dependency: transitive + description: + name: app_links_platform_interface + sha256: "7546f09a6e93f4a2df2fe2bd40a5c6c64310ac461b036d82b43033be7a59f809" + url: "https://pub.dev" + source: hosted + version: "2.0.4" + app_links_web: + dependency: transitive + description: + name: app_links_web + sha256: af060ed76183f9e2b87510a9480e56a5352b6c249778d07bd2c95fc35632a555 + url: "https://pub.dev" + source: hosted + version: "1.0.4" args: dependency: transitive description: @@ -653,6 +685,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.5.12" + gtk: + dependency: transitive + description: + name: gtk + sha256: "4ff85b2a16724029dd9e5bbb5a94b6918f9973f74ba571c949d2002801879cf5" + url: "https://pub.dev" + source: hosted + version: "2.2.0" hexcolor: dependency: "direct main" description: @@ -1539,5 +1579,5 @@ packages: source: hosted version: "6.5.0" sdks: - dart: ">=3.10.0-0 <4.0.0" - flutter: ">=3.35.0" + dart: ">=3.12.0 <4.0.0" + flutter: ">=3.44.0" diff --git a/pubspec.yaml b/pubspec.yaml index a5071ba..cce50fe 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -64,6 +64,7 @@ dependencies: local_auth: ^2.2.0 # huawei_fido: ^6.3.0+305 device_info_plus: ^10.1.0 + app_links: ^7.2.1 dev_dependencies: