Bug Fixes

master
FaizHashmi 2 weeks ago
parent 6c71c2095e
commit dfbf5069f2

1
.gitignore vendored

@ -1,3 +1,4 @@
# Miscellaneous # Miscellaneous
*.class *.class
*.log *.log

@ -150,6 +150,7 @@
"my_service_providers": "مزودي الخدمة الجدد", "my_service_providers": "مزودي الخدمة الجدد",
"appointment_details": "تفاصيل الموعد", "appointment_details": "تفاصيل الموعد",
"no_city_available": "لا توجد مدينة متاحة لهذا البلد", "no_city_available": "لا توجد مدينة متاحة لهذا البلد",
"noDataAvailable": "لا توجد بيانات متاحة",
"branch_created": "تم إنشاء الفرع بنجاح", "branch_created": "تم إنشاء الفرع بنجاح",
"branch_updated": "تم تحديث الفرع بنجاح", "branch_updated": "تم تحديث الفرع بنجاح",
"branch_deleted": "تم حذف الفرع بنجاح", "branch_deleted": "تم حذف الفرع بنجاح",
@ -334,6 +335,7 @@
"vehiclePictures": "صور المركبة", "vehiclePictures": "صور المركبة",
"vehiclePart": "جزء المركبة", "vehiclePart": "جزء المركبة",
"damagePartPictures": "صور الأجزاء المتضررة", "damagePartPictures": "صور الأجزاء المتضررة",
"qrCode": "رمز الاستجابة السريعة",
"vehicleDamagePart": "جزء المركبة المتضرر", "vehicleDamagePart": "جزء المركبة المتضرر",
"duration": "المدة", "duration": "المدة",
"specialServices": "الخدمات الخاصة", "specialServices": "الخدمات الخاصة",
@ -385,6 +387,7 @@
"error": "خطأ", "error": "خطأ",
"appointmentBookedSuccessfully": "تم حجز موعدك بنجاح", "appointmentBookedSuccessfully": "تم حجز موعدك بنجاح",
"appointmentStatusUpdated": "تم تحديث حالة الموعد", "appointmentStatusUpdated": "تم تحديث حالة الموعد",
"appointmentStatusLabel": "حالة الموعد",
"paymentStatusUpdated": "تم تحديث حالة الدفع", "paymentStatusUpdated": "تم تحديث حالة الدفع",
"serviceStatusUpdated": "تم تحديث حالة الخدمة", "serviceStatusUpdated": "تم تحديث حالة الخدمة",
"selectOneSubService": "يرجى اختيار خدمة فرعية واحدة على الأقل", "selectOneSubService": "يرجى اختيار خدمة فرعية واحدة على الأقل",
@ -664,6 +667,7 @@
"searchByRequestedDate": "البحث حسب تاريخ الطلب", "searchByRequestedDate": "البحث حسب تاريخ الطلب",
"selectDate": "اختر التاريخ", "selectDate": "اختر التاريخ",
"searchByVehicleModel": "البحث حسب طراز المركبة", "searchByVehicleModel": "البحث حسب طراز المركبة",
"selectVehicleModel": "اختر طراز المركبة",
"customerCarePrompt": "هل لديك أي استفسار؟ نحن هنا لخدمتك. انقر هنا للتحدث مع خدمة العملاء لدينا الآن.", "customerCarePrompt": "هل لديك أي استفسار؟ نحن هنا لخدمتك. انقر هنا للتحدث مع خدمة العملاء لدينا الآن.",
"callNow": "اتصل الآن", "callNow": "اتصل الآن",
"condition": "الحالة", "condition": "الحالة",

@ -153,6 +153,7 @@
"my_service_providers": "My Recent Service Providers", "my_service_providers": "My Recent Service Providers",
"appointment_details": "Appointment Details", "appointment_details": "Appointment Details",
"no_city_available": "No City Available for this country", "no_city_available": "No City Available for this country",
"noDataAvailable": "No Data Available",
"branch_created": "Branch is successfully created", "branch_created": "Branch is successfully created",
"branch_updated": "Branch is successfully Updated", "branch_updated": "Branch is successfully Updated",
"branch_deleted": "Branch is successfully Deleted", "branch_deleted": "Branch is successfully Deleted",
@ -337,6 +338,7 @@
"vehiclePictures": "Vehicle Pictures", "vehiclePictures": "Vehicle Pictures",
"vehiclePart": "Vehicle Part", "vehiclePart": "Vehicle Part",
"damagePartPictures": "Damage Part Pictures", "damagePartPictures": "Damage Part Pictures",
"qrCode": "QR Code",
"vehicleDamagePart": "Vehicle Damage Part", "vehicleDamagePart": "Vehicle Damage Part",
"duration": "Duration", "duration": "Duration",
"specialServices": "Special Services", "specialServices": "Special Services",
@ -388,6 +390,7 @@
"error": "Error", "error": "Error",
"appointmentBookedSuccessfully": "Your appointment has been booked successfully", "appointmentBookedSuccessfully": "Your appointment has been booked successfully",
"appointmentStatusUpdated": "Appointment Status Updated", "appointmentStatusUpdated": "Appointment Status Updated",
"appointmentStatusLabel": "Appointment Status",
"paymentStatusUpdated": "Payment Status Updated", "paymentStatusUpdated": "Payment Status Updated",
"serviceStatusUpdated": "Service Status Updated", "serviceStatusUpdated": "Service Status Updated",
"selectOneSubService": "Please select at least one sub service", "selectOneSubService": "Please select at least one sub service",
@ -668,6 +671,7 @@
"searchByRequestedDate": "Search by Requested Date", "searchByRequestedDate": "Search by Requested Date",
"selectDate": "Select Date", "selectDate": "Select Date",
"searchByVehicleModel": "Search By Vehicle Model", "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.", "customerCarePrompt": "Got any question? We are here to service. Click here to talk to our customer care right now.",
"callNow": "Call Now", "callNow": "Call Now",
"condition": "Condition", "condition": "Condition",

@ -124,13 +124,14 @@ class ApiClientImp implements ApiClient {
return await _postForResponse(url, requestBody, token: token, queryParameters: queryParameters, headers: headers, retryTimes: retryTimes); return await _postForResponse(url, requestBody, token: token, queryParameters: queryParameters, headers: headers, retryTimes: retryTimes);
} }
Future<Response> _postForResponse(String url, Future<Response> _postForResponse(
requestBody, { String url,
String? token, requestBody, {
Map<String, dynamic>? queryParameters, String? token,
Map<String, String>? headers, Map<String, dynamic>? queryParameters,
int retryTimes = 0, Map<String, String>? headers,
}) async { int retryTimes = 0,
}) async {
try { try {
var headers0 = <String, String>{}; var headers0 = <String, String>{};
@ -158,7 +159,7 @@ class ApiClientImp implements ApiClient {
logger.i(url); logger.i(url);
logger.d("------Payload------"); logger.d("------Payload------");
logger.i(jsonDecode(requestBody)); logger.i(jsonDecode(requestBody));
// logger.d("------Response------"); logger.d("------Response------");
logger.i(jsonDecode(response.body)); logger.i(jsonDecode(response.body));
} }
if (response.statusCode >= 200 && response.statusCode < 500) { if (response.statusCode >= 200 && response.statusCode < 500) {
@ -210,8 +211,7 @@ class ApiClientImp implements ApiClient {
bool _certificateCheck(X509Certificate cert, String host, int port) => true; bool _certificateCheck(X509Certificate cert, String host, int port) => true;
Future<T> _withClient<T>(Future<T> Function(Client) fn) async { Future<T> _withClient<T>(Future<T> Function(Client) fn) async {
var httpClient = HttpClient() var httpClient = HttpClient()..badCertificateCallback = _certificateCheck;
..badCertificateCallback = _certificateCheck;
var client = IOClient(httpClient); var client = IOClient(httpClient);
try { try {
return await fn(client); return await fn(client);
@ -271,8 +271,10 @@ class ApiClientImp implements ApiClient {
} }
if (!kReleaseMode) { if (!kReleaseMode) {
logger.i(url); log("---URL---");
logger.i("$queryParameters"); log(url);
log("---Params---");
log("$queryParameters");
} }
var response = await _get(Uri.parse(url), headers: headers0).timeout(const Duration(seconds: 120)); var response = await _get(Uri.parse(url), headers: headers0).timeout(const Duration(seconds: 120));

@ -166,6 +166,7 @@ class CodegenLoader extends AssetLoader{
"my_service_providers": "مزودي الخدمة الجدد", "my_service_providers": "مزودي الخدمة الجدد",
"appointment_details": "تفاصيل الموعد", "appointment_details": "تفاصيل الموعد",
"no_city_available": "لا توجد مدينة متاحة لهذا البلد", "no_city_available": "لا توجد مدينة متاحة لهذا البلد",
"noDataAvailable": "لا توجد بيانات متاحة",
"branch_created": "تم إنشاء الفرع بنجاح", "branch_created": "تم إنشاء الفرع بنجاح",
"branch_updated": "تم تحديث الفرع بنجاح", "branch_updated": "تم تحديث الفرع بنجاح",
"branch_deleted": "تم حذف الفرع بنجاح", "branch_deleted": "تم حذف الفرع بنجاح",
@ -350,6 +351,7 @@ class CodegenLoader extends AssetLoader{
"vehiclePictures": "صور المركبة", "vehiclePictures": "صور المركبة",
"vehiclePart": "جزء المركبة", "vehiclePart": "جزء المركبة",
"damagePartPictures": "صور الأجزاء المتضررة", "damagePartPictures": "صور الأجزاء المتضررة",
"qrCode": "رمز الاستجابة السريعة",
"vehicleDamagePart": "جزء المركبة المتضرر", "vehicleDamagePart": "جزء المركبة المتضرر",
"duration": "المدة", "duration": "المدة",
"specialServices": "الخدمات الخاصة", "specialServices": "الخدمات الخاصة",
@ -401,6 +403,7 @@ class CodegenLoader extends AssetLoader{
"error": "خطأ", "error": "خطأ",
"appointmentBookedSuccessfully": "تم حجز موعدك بنجاح", "appointmentBookedSuccessfully": "تم حجز موعدك بنجاح",
"appointmentStatusUpdated": "تم تحديث حالة الموعد", "appointmentStatusUpdated": "تم تحديث حالة الموعد",
"appointmentStatusLabel": "حالة الموعد",
"paymentStatusUpdated": "تم تحديث حالة الدفع", "paymentStatusUpdated": "تم تحديث حالة الدفع",
"serviceStatusUpdated": "تم تحديث حالة الخدمة", "serviceStatusUpdated": "تم تحديث حالة الخدمة",
"selectOneSubService": "يرجى اختيار خدمة فرعية واحدة على الأقل", "selectOneSubService": "يرجى اختيار خدمة فرعية واحدة على الأقل",
@ -680,6 +683,7 @@ class CodegenLoader extends AssetLoader{
"searchByRequestedDate": "البحث حسب تاريخ الطلب", "searchByRequestedDate": "البحث حسب تاريخ الطلب",
"selectDate": "اختر التاريخ", "selectDate": "اختر التاريخ",
"searchByVehicleModel": "البحث حسب طراز المركبة", "searchByVehicleModel": "البحث حسب طراز المركبة",
"selectVehicleModel": "اختر طراز المركبة",
"customerCarePrompt": "هل لديك أي استفسار؟ نحن هنا لخدمتك. انقر هنا للتحدث مع خدمة العملاء لدينا الآن.", "customerCarePrompt": "هل لديك أي استفسار؟ نحن هنا لخدمتك. انقر هنا للتحدث مع خدمة العملاء لدينا الآن.",
"callNow": "اتصل الآن", "callNow": "اتصل الآن",
"condition": "الحالة", "condition": "الحالة",
@ -1088,6 +1092,7 @@ static const Map<String,dynamic> _en_US = {
"my_service_providers": "My Recent Service Providers", "my_service_providers": "My Recent Service Providers",
"appointment_details": "Appointment Details", "appointment_details": "Appointment Details",
"no_city_available": "No City Available for this country", "no_city_available": "No City Available for this country",
"noDataAvailable": "No Data Available",
"branch_created": "Branch is successfully created", "branch_created": "Branch is successfully created",
"branch_updated": "Branch is successfully Updated", "branch_updated": "Branch is successfully Updated",
"branch_deleted": "Branch is successfully Deleted", "branch_deleted": "Branch is successfully Deleted",
@ -1272,6 +1277,7 @@ static const Map<String,dynamic> _en_US = {
"vehiclePictures": "Vehicle Pictures", "vehiclePictures": "Vehicle Pictures",
"vehiclePart": "Vehicle Part", "vehiclePart": "Vehicle Part",
"damagePartPictures": "Damage Part Pictures", "damagePartPictures": "Damage Part Pictures",
"qrCode": "QR Code",
"vehicleDamagePart": "Vehicle Damage Part", "vehicleDamagePart": "Vehicle Damage Part",
"duration": "Duration", "duration": "Duration",
"specialServices": "Special Services", "specialServices": "Special Services",
@ -1323,6 +1329,7 @@ static const Map<String,dynamic> _en_US = {
"error": "Error", "error": "Error",
"appointmentBookedSuccessfully": "Your appointment has been booked successfully", "appointmentBookedSuccessfully": "Your appointment has been booked successfully",
"appointmentStatusUpdated": "Appointment Status Updated", "appointmentStatusUpdated": "Appointment Status Updated",
"appointmentStatusLabel": "Appointment Status",
"paymentStatusUpdated": "Payment Status Updated", "paymentStatusUpdated": "Payment Status Updated",
"serviceStatusUpdated": "Service Status Updated", "serviceStatusUpdated": "Service Status Updated",
"selectOneSubService": "Please select at least one sub service", "selectOneSubService": "Please select at least one sub service",
@ -1603,6 +1610,7 @@ static const Map<String,dynamic> _en_US = {
"searchByRequestedDate": "Search by Requested Date", "searchByRequestedDate": "Search by Requested Date",
"selectDate": "Select Date", "selectDate": "Select Date",
"searchByVehicleModel": "Search By Vehicle Model", "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.", "customerCarePrompt": "Got any question? We are here to service. Click here to talk to our customer care right now.",
"callNow": "Call Now", "callNow": "Call Now",
"condition": "Condition", "condition": "Condition",

@ -132,6 +132,7 @@ abstract class LocaleKeys {
static const my_service_providers = 'my_service_providers'; static const my_service_providers = 'my_service_providers';
static const appointment_details = 'appointment_details'; static const appointment_details = 'appointment_details';
static const no_city_available = 'no_city_available'; static const no_city_available = 'no_city_available';
static const noDataAvailable = 'noDataAvailable';
static const branch_created = 'branch_created'; static const branch_created = 'branch_created';
static const branch_updated = 'branch_updated'; static const branch_updated = 'branch_updated';
static const branch_deleted = 'branch_deleted'; static const branch_deleted = 'branch_deleted';
@ -316,6 +317,7 @@ abstract class LocaleKeys {
static const vehiclePictures = 'vehiclePictures'; static const vehiclePictures = 'vehiclePictures';
static const vehiclePart = 'vehiclePart'; static const vehiclePart = 'vehiclePart';
static const damagePartPictures = 'damagePartPictures'; static const damagePartPictures = 'damagePartPictures';
static const qrCode = 'qrCode';
static const vehicleDamagePart = 'vehicleDamagePart'; static const vehicleDamagePart = 'vehicleDamagePart';
static const duration = 'duration'; static const duration = 'duration';
static const specialServices = 'specialServices'; static const specialServices = 'specialServices';
@ -366,6 +368,7 @@ abstract class LocaleKeys {
static const error = 'error'; static const error = 'error';
static const appointmentBookedSuccessfully = 'appointmentBookedSuccessfully'; static const appointmentBookedSuccessfully = 'appointmentBookedSuccessfully';
static const appointmentStatusUpdated = 'appointmentStatusUpdated'; static const appointmentStatusUpdated = 'appointmentStatusUpdated';
static const appointmentStatusLabel = 'appointmentStatusLabel';
static const paymentStatusUpdated = 'paymentStatusUpdated'; static const paymentStatusUpdated = 'paymentStatusUpdated';
static const serviceStatusUpdated = 'serviceStatusUpdated'; static const serviceStatusUpdated = 'serviceStatusUpdated';
static const selectOneSubService = 'selectOneSubService'; static const selectOneSubService = 'selectOneSubService';
@ -645,6 +648,7 @@ abstract class LocaleKeys {
static const searchByRequestedDate = 'searchByRequestedDate'; static const searchByRequestedDate = 'searchByRequestedDate';
static const selectDate = 'selectDate'; static const selectDate = 'selectDate';
static const searchByVehicleModel = 'searchByVehicleModel'; static const searchByVehicleModel = 'searchByVehicleModel';
static const selectVehicleModel = 'selectVehicleModel';
static const customerCarePrompt = 'customerCarePrompt'; static const customerCarePrompt = 'customerCarePrompt';
static const callNow = 'callNow'; static const callNow = 'callNow';
static const condition = 'condition'; static const condition = 'condition';

@ -14,6 +14,8 @@ class AdDetailsModel {
// List<Null>? reserved; // List<Null>? reserved;
int? statusID; int? statusID;
//TODO: Overwrite these with StatusLabelDisplay and StatusLabelDisplayN
String? statuslabel; String? statuslabel;
String? statuslabelN; String? statuslabelN;
double? adsDurationPrice; double? adsDurationPrice;
@ -137,8 +139,8 @@ class AdDetailsModel {
}); });
} }
statusID = json['statusID']; statusID = json['statusID'];
statuslabel = json['statuslabel']; statuslabel = json['statuslabelDisplay'] ?? json['statuslabel'];
statuslabelN = json['statuslabelN']; statuslabelN = json['statuslabelDisplayN'] ?? json['statuslabelN'] ?? json['statuslabelDisplay'] ?? json['statuslabel'];
adsDurationPrice = json['adsDurationPrice']; adsDurationPrice = json['adsDurationPrice'];
adsDurationDiscount = json['adsDurationDiscount']; adsDurationDiscount = json['adsDurationDiscount'];
adsDurationDiscountPrice = json['adsDurationDiscountPrice']; adsDurationDiscountPrice = json['adsDurationDiscountPrice'];

@ -20,8 +20,10 @@ class EnumsModel {
factory EnumsModel.fromJson(Map<String, dynamic> json) => EnumsModel( factory EnumsModel.fromJson(Map<String, dynamic> json) => EnumsModel(
id: json["id"], id: json["id"],
enumTypeId: json["enumTypeID"], enumTypeId: json["enumTypeID"],
enumValueStr: json["enumValueStr"], // enumValueStr: json["enumValueStr"],
enumValueStrN: json["enumValueStrN"] ?? 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"], enumValueStrDes: json["enumValueStrDes"] ?? json["enumValueStr"],
enumValue: json["enumValue"], enumValue: json["enumValue"],
isActive: json["isActive"], isActive: json["isActive"],

@ -44,6 +44,7 @@ abstract class AdsRepo {
List<String>? vehicleModelYearIdsList, List<String>? vehicleModelYearIdsList,
List<String>? vehicleTypeIdsList, List<String>? vehicleTypeIdsList,
List<String>? vehicleBrandIdsList, List<String>? vehicleBrandIdsList,
List<String>? vehicleModelIdsList,
List<String>? createdByRolesIdsList, List<String>? createdByRolesIdsList,
List<String>? vehicleAdConditionIdsList, List<String>? vehicleAdConditionIdsList,
List<String>? vehicleAdCreatedDateList, List<String>? vehicleAdCreatedDateList,
@ -66,6 +67,8 @@ abstract class AdsRepo {
Future<GenericRespModel> toggleAdFavorite({required int adsID, required bool isFavorite}); Future<GenericRespModel> toggleAdFavorite({required int adsID, required bool isFavorite});
Future<AdDetailsModel?> getAdDetailsById({required int adId});
Future<AdsBankDetailsModel?> getAdBankingAccountInfo({required int adId}); Future<AdsBankDetailsModel?> getAdBankingAccountInfo({required int adId});
Future<GenericRespModel> updateAdStatus({required int adId, required AdPostStatus adStatusToUpdate, String? comment}); Future<GenericRespModel> updateAdStatus({required int adId, required AdPostStatus adStatusToUpdate, String? comment});
@ -397,6 +400,7 @@ class AdsRepoImp implements AdsRepo {
List<String>? vehicleModelYearIdsList, List<String>? vehicleModelYearIdsList,
List<String>? vehicleTypeIdsList, List<String>? vehicleTypeIdsList,
List<String>? vehicleBrandIdsList, List<String>? vehicleBrandIdsList,
List<String>? vehicleModelIdsList,
List<String>? createdByRolesIdsList, List<String>? createdByRolesIdsList,
List<String>? vehicleAdConditionIdsList, List<String>? vehicleAdConditionIdsList,
List<String>? vehicleAdCreatedDateList, List<String>? vehicleAdCreatedDateList,
@ -411,6 +415,7 @@ class AdsRepoImp implements AdsRepo {
"CityIDs": cityIdsList ?? [], "CityIDs": cityIdsList ?? [],
"VehicleTypeIDs": vehicleTypeIdsList ?? [], "VehicleTypeIDs": vehicleTypeIdsList ?? [],
"VehicleBrandIDs": vehicleBrandIdsList ?? [], "VehicleBrandIDs": vehicleBrandIdsList ?? [],
"VehicleModelIDs": vehicleModelIdsList ?? [],
"VehicleModelYearIDs": vehicleModelYearIdsList ?? [], "VehicleModelYearIDs": vehicleModelYearIdsList ?? [],
"CreatedByRoles": createdByRolesIdsList ?? [], "CreatedByRoles": createdByRolesIdsList ?? [],
"AdsStatuses": ["${AdPostStatus.active.getIdFromAdPostStatusEnum()}"], //only Active ADS "AdsStatuses": ["${AdPostStatus.active.getIdFromAdPostStatusEnum()}"], //only Active ADS
@ -560,11 +565,7 @@ class AdsRepoImp implements AdsRepo {
} }
@override @override
Future<List<AdDetailsModel>> getFavouriteAds({ Future<List<AdDetailsModel>> getFavouriteAds({AdPostStatus? adPostStatus, CreatedByRoleEnum? createdByRoleEnum, int? vehicleBrandId}) async {
AdPostStatus? adPostStatus,
CreatedByRoleEnum? createdByRoleEnum,
int? vehicleBrandId,
}) async {
// Build filter parameters similar to getAllAds // Build filter parameters similar to getAllAds
Map<String, dynamic> params = { Map<String, dynamic> params = {
"PageSize": "30", "PageSize": "30",
@ -604,9 +605,15 @@ class AdsRepoImp implements AdsRepo {
@override @override
Future<GenericRespModel> toggleAdFavorite({required int adsID, required bool isFavorite}) async { Future<GenericRespModel> toggleAdFavorite({required int adsID, required bool isFavorite}) async {
var params = { var params = {
"ID": adsID.toString(), "adsID": adsID.toString(),
}; };
if (isFavorite) {
params = {
"ID": adsID.toString(),
};
}
String endpoint = isFavorite ? ApiConsts.unFavouriteAds : ApiConsts.favouriteAdsCreate; String endpoint = isFavorite ? ApiConsts.unFavouriteAds : ApiConsts.favouriteAdsCreate;
GenericRespModel genericRespModel = await apiClient.postJsonForObject( GenericRespModel genericRespModel = await apiClient.postJsonForObject(
@ -619,6 +626,18 @@ class AdsRepoImp implements AdsRepo {
return genericRespModel; return genericRespModel;
} }
@override
Future<AdDetailsModel?> getAdDetailsById({required int adId}) async {
try {
List<AdDetailsModel> ads = await getAdsPerSpecificIds(ids: [adId.toString()], reservedAds: []);
if (ads.isEmpty) return null;
return ads.first;
} catch (e) {
logger.e(e.toString());
return null;
}
}
@override @override
Future<AdsBankDetailsModel?> getAdBankingAccountInfo({required int adId}) async { Future<AdsBankDetailsModel?> getAdBankingAccountInfo({required int adId}) async {
var params = { var params = {

@ -304,8 +304,7 @@ class AppointmentRepoImp implements AppointmentRepo {
queryParameters: params, queryParameters: params,
ApiConsts.customersAppointmentGetByFilters, ApiConsts.customersAppointmentGetByFilters,
); );
print("genericRespModel.data .......");
print(genericRespModel.data);
List<AppointmentListModel> appointmentList = List.generate(genericRespModel.data.length, (index) => AppointmentListModel.fromJson(genericRespModel.data[index])); List<AppointmentListModel> appointmentList = List.generate(genericRespModel.data.length, (index) => AppointmentListModel.fromJson(genericRespModel.data[index]));
return appointmentList; return appointmentList;
} }

@ -257,7 +257,7 @@ class CommonRepoImp implements CommonRepo {
token: appState.getUser.data!.accessToken, token: appState.getUser.data!.accessToken,
(json) => GenericRespModel.fromJson(json), (json) => GenericRespModel.fromJson(json),
ApiConsts.vehicleModelGet, ApiConsts.vehicleModelGet,
queryParameters: postParams, queryParameters: vehicleTypeId != -1 ? postParams : null,
); );
List<VehicleModel> vehicleModels = List.generate(adsGenericModel.data.length, (index) => VehicleModel.fromJson(adsGenericModel.data[index])); List<VehicleModel> vehicleModels = List.generate(adsGenericModel.data.length, (index) => VehicleModel.fromJson(adsGenericModel.data[index]));
return vehicleModels; return vehicleModels;

@ -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<Uri>? _subscription;
/// Call once, after the app's first frame (so [navigatorKey] is attached to a Navigator).
static Future<void> 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<void> _openAd(int adId) async {
final context = navigatorKey.currentContext;
if (context == null) return;
Utils.showLoading(context);
final adDetails = await context.read<AdVM>().getAdDetailsById(adId);
Utils.hideLoading(context);
if (adDetails == null) {
Utils.showToast(LocaleKeys.somethingWrong.tr());
return;
}
navigateWithName(context, AppRoutes.adsDetailView, arguments: adDetails);
}
}

@ -378,6 +378,15 @@ class AdVM extends BaseVM {
} }
} }
Future<AdDetailsModel?> getAdDetailsById(int adId) async {
try {
return await adsRepo.getAdDetailsById(adId: adId);
} catch (e) {
logger.e(e.toString());
return null;
}
}
Future<void> getExploreAds() async { Future<void> getExploreAds() async {
pageIndexForExploreAds = 1; pageIndexForExploreAds = 1;
hasMoreDataForExploreAds = true; hasMoreDataForExploreAds = true;
@ -727,6 +736,7 @@ class AdVM extends BaseVM {
if (!ifAlreadyExist(list: vehicleBrandsAdSearchHistory, value: brandValue)) { if (!ifAlreadyExist(list: vehicleBrandsAdSearchHistory, value: brandValue)) {
addToVehicleBrandsAdSearchHistory(value: brandValue); addToVehicleBrandsAdSearchHistory(value: brandValue);
} }
pruneVehicleModelsAdSearchHistoryForRemovedBrands();
notifyListeners(); notifyListeners();
return; return;
} }
@ -739,7 +749,16 @@ class AdVM extends BaseVM {
SelectionModel vehicleModelId = SelectionModel(selectedOption: "", selectedId: -1, errorValue: ""); 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; vehicleModelId = id;
notifyListeners(); notifyListeners();
} }
@ -1250,7 +1269,7 @@ class AdVM extends BaseVM {
isInUsedError = ""; isInUsedError = "";
} }
if ((pickedPostingImages.isEmpty || pickedPostingImages.length < 3) && !isAdEditEnabled) { if (pickedPostingImages.isEmpty || (pickedPostingImages.length < 3 && !isAdEditEnabled)) {
vehicleImageError = GlobalConsts.attachImageError; vehicleImageError = GlobalConsts.attachImageError;
isValidated = false; isValidated = false;
} else { } else {
@ -2321,6 +2340,15 @@ class AdVM extends BaseVM {
List<VehicleTypeModel> vehicleTypesForFilters = []; List<VehicleTypeModel> vehicleTypesForFilters = [];
List<VehicleModel> vehicleModelsForFilters = [];
// Cascading list: only models belonging to the brand(s) currently selected in vehicleBrandsAdSearchHistory.
List<VehicleModel> get vehicleModelsForSelectedBrandsFilter {
if (vehicleBrandsAdSearchHistory.isEmpty) return [];
Set<int> selectedBrandIds = vehicleBrandsAdSearchHistory.map((e) => e.id).toSet();
return vehicleModelsForFilters.where((model) => selectedBrandIds.contains(model.vehicleBrandID)).toList();
}
Future<void> populateDataForAdFilter() async { Future<void> populateDataForAdFilter() async {
setState(ViewState.busy); setState(ViewState.busy);
pageIndexForExploreAds = 1; pageIndexForExploreAds = 1;
@ -2349,6 +2377,10 @@ class AdVM extends BaseVM {
vehicleModelYears = await commonRepo.getVehicleModelYears(vehicleTypeId: -1); 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 vehicleCities = await commonRepo.getVehicleCities(countryId: -1); // fetch all the cities
setState(ViewState.idle); setState(ViewState.idle);
} }
@ -2492,6 +2524,7 @@ class AdVM extends BaseVM {
void removeVehicleBrandsAdSearchHistory({bool isClear = false, required int index}) { void removeVehicleBrandsAdSearchHistory({bool isClear = false, required int index}) {
if (isClear) { if (isClear) {
vehicleBrandsAdSearchHistory.clear(); vehicleBrandsAdSearchHistory.clear();
pruneVehicleModelsAdSearchHistoryForRemovedBrands();
notifyListeners(); notifyListeners();
return; return;
} }
@ -2499,6 +2532,7 @@ class AdVM extends BaseVM {
if (vehicleBrandsAdSearchHistory.isEmpty) { if (vehicleBrandsAdSearchHistory.isEmpty) {
updateAdsFiltersCounter(adsFiltersCounter - 1); updateAdsFiltersCounter(adsFiltersCounter - 1);
} }
pruneVehicleModelsAdSearchHistoryForRemovedBrands();
notifyListeners(); notifyListeners();
} }
@ -2510,6 +2544,42 @@ class AdVM extends BaseVM {
notifyListeners(); notifyListeners();
} }
// MODELS (cascades from vehicleBrandsAdSearchHistory)
List<DropValue> 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<int> 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 // LOCATION
List<DropValue> vehicleLocationAdSearchHistory = []; List<DropValue> vehicleLocationAdSearchHistory = [];
@ -2675,6 +2745,7 @@ class AdVM extends BaseVM {
vehicleAdConditionSearchHistory.clear(); vehicleAdConditionSearchHistory.clear();
adFilterMobilePhoneSearchHistory.clear(); adFilterMobilePhoneSearchHistory.clear();
vehicleBrandsAdSearchHistory.clear(); vehicleBrandsAdSearchHistory.clear();
vehicleModelsAdSearchHistory.clear();
adFilterDemandStartPrice = ''; adFilterDemandStartPrice = '';
adFilterDemandEndPrice = ''; adFilterDemandEndPrice = '';
clearAdFilterSelections(); // Clear dropdown selections clearAdFilterSelections(); // Clear dropdown selections
@ -2730,6 +2801,13 @@ class AdVM extends BaseVM {
} }
} }
List<String> modelsIdsList = [];
if (vehicleModelsAdSearchHistory.isNotEmpty) {
for (var element in vehicleModelsAdSearchHistory) {
modelsIdsList.add(element.id.toString());
}
}
List<String> vehicleYearIdsList = []; List<String> vehicleYearIdsList = [];
if (vehicleYearAdSearchHistory.isNotEmpty) { if (vehicleYearAdSearchHistory.isNotEmpty) {
for (var element in vehicleYearAdSearchHistory) { for (var element in vehicleYearAdSearchHistory) {
@ -2794,6 +2872,7 @@ class AdVM extends BaseVM {
createdByRolesIdsList: adOwnerIdsList, createdByRolesIdsList: adOwnerIdsList,
vehicleTypeIdsList: typesIdsList, vehicleTypeIdsList: typesIdsList,
vehicleBrandIdsList: brandsIdsList, vehicleBrandIdsList: brandsIdsList,
vehicleModelIdsList: modelsIdsList,
vehicleModelYearIdsList: vehicleYearIdsList, vehicleModelYearIdsList: vehicleYearIdsList,
vehicleAdConditionIdsList: conditionsIdsList, vehicleAdConditionIdsList: conditionsIdsList,
vehicleAdCreatedDateList: createdDatesList, vehicleAdCreatedDateList: createdDatesList,

@ -307,8 +307,23 @@ class AppointmentsVM extends BaseVM {
notifyListeners(); 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) { void removeServiceInCurrentAppointment(int index) {
int serviceId = servicesInCurrentAppointment.elementAt(index).serviceProviderServiceId ?? -1; int serviceId = servicesInCurrentAppointment
.elementAt(index)
.serviceProviderServiceId ?? -1;
allSelectedItemsInAppointments.removeWhere((element) => element.serviceProviderServiceId == serviceId); allSelectedItemsInAppointments.removeWhere((element) => element.serviceProviderServiceId == serviceId);
servicesInCurrentAppointment[index].serviceItems!.clear(); servicesInCurrentAppointment[index].serviceItems!.clear();
servicesInCurrentAppointment.removeAt(index); servicesInCurrentAppointment.removeAt(index);
@ -371,8 +386,8 @@ class AppointmentsVM extends BaseVM {
if (shouldPopulateUpcoming) { if (shouldPopulateUpcoming) {
myUpComingAppointments = myFilteredAppointmentsForProvider myUpComingAppointments = myFilteredAppointmentsForProvider
.where((element) => .where((element) =>
(element.appointmentStatusEnum == AppointmentStatusEnum.booked || element.appointmentStatusEnum == AppointmentStatusEnum.confirmed) && (element.appointmentStatusEnum == AppointmentStatusEnum.booked || element.appointmentStatusEnum == AppointmentStatusEnum.confirmed) &&
(DateHelper.parseStringToDate(element.appointmentDate!).isAfter(DateTime.now()))) (DateHelper.parseStringToDate(element.appointmentDate!).isAfter(DateTime.now())))
.toList(); .toList();
log("myUpComingAppointments: ${myUpComingAppointments.length}"); log("myUpComingAppointments: ${myUpComingAppointments.length}");
} }
@ -401,8 +416,8 @@ class AppointmentsVM extends BaseVM {
if (shouldPopulateUpcoming) { if (shouldPopulateUpcoming) {
myUpComingAppointments = myFilteredAppointmentsForCustomers myUpComingAppointments = myFilteredAppointmentsForCustomers
.where((element) => .where((element) =>
(element.appointmentStatusEnum == AppointmentStatusEnum.booked || element.appointmentStatusEnum == AppointmentStatusEnum.confirmed) && (element.appointmentStatusEnum == AppointmentStatusEnum.booked || element.appointmentStatusEnum == AppointmentStatusEnum.confirmed) &&
(DateHelper.parseStringToDate(element.appointmentDate!).isAfter(DateTime.now()))) (DateHelper.parseStringToDate(element.appointmentDate!).isAfter(DateTime.now())))
.toList(); .toList();
} }
@ -417,9 +432,13 @@ class AppointmentsVM extends BaseVM {
List<AppointmentListModel> groupedList = uniqueCustomerIDs.map((id) { List<AppointmentListModel> groupedList = uniqueCustomerIDs.map((id) {
List<AppointmentListModel> list = appointments.where((item) => item.customerID == id).toList(); List<AppointmentListModel> list = appointments.where((item) => item.customerID == id).toList();
list.sort((a, b) => DateHelper.parseStringToDate(DateHelper.formatDateT(b.appointmentDate!)) list.sort((a, b) =>
.millisecondsSinceEpoch DateHelper
.compareTo(DateHelper.parseStringToDate(DateHelper.formatDateT(a.appointmentDate!)).millisecondsSinceEpoch)); .parseStringToDate(DateHelper.formatDateT(b.appointmentDate!))
.millisecondsSinceEpoch
.compareTo(DateHelper
.parseStringToDate(DateHelper.formatDateT(a.appointmentDate!))
.millisecondsSinceEpoch));
AppointmentListModel model = list.first; AppointmentListModel model = list.first;
model.customerAppointmentList = list; model.customerAppointmentList = list;
return model; return model;
@ -428,6 +447,35 @@ class AppointmentsVM extends BaseVM {
return groupedList; 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<int?> refreshUpcomingAppointmentAndGetIndex(AppointmentListModel tappedAppointment) async {
int branchId = tappedAppointment.branchId ?? 0;
List<AppointmentListModel> freshAppointments = await getAppointmentsBasedOnFiltersForProviders(
branchID: branchId,
appointmentStatus: AppointmentStatusEnum.allAppointments,
);
List<AppointmentListModel> upcomingOnly = freshAppointments
.where((element) =>
(element.appointmentStatusEnum == AppointmentStatusEnum.booked || element.appointmentStatusEnum == AppointmentStatusEnum.confirmed) &&
(DateHelper.parseStringToDate(element.appointmentDate!).isAfter(DateTime.now())))
.toList();
List<AppointmentListModel> 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; AppointmentSlots? appointmentSlots;
bool isSlotsLoading = true; bool isSlotsLoading = true;
@ -506,7 +554,7 @@ class AppointmentsVM extends BaseVM {
void updateCheckBoxInMergeRequest(int currentIndex) { void updateCheckBoxInMergeRequest(int currentIndex) {
myFilteredAppointmentsForProvider[selectedAppointmentIndex].customerAppointmentList![currentIndex].isSelected = myFilteredAppointmentsForProvider[selectedAppointmentIndex].customerAppointmentList![currentIndex].isSelected =
!(myFilteredAppointmentsForProvider[selectedAppointmentIndex].customerAppointmentList?[currentIndex].isSelected ?? false); !(myFilteredAppointmentsForProvider[selectedAppointmentIndex].customerAppointmentList?[currentIndex].isSelected ?? false);
int count = countSelected(myFilteredAppointmentsForProvider[selectedAppointmentIndex].customerAppointmentList ?? []); int count = countSelected(myFilteredAppointmentsForProvider[selectedAppointmentIndex].customerAppointmentList ?? []);
if (count > 1) { if (count > 1) {
@ -518,7 +566,10 @@ class AppointmentsVM extends BaseVM {
} }
int countSelected(List<AppointmentListModel> appointments) { int countSelected(List<AppointmentListModel> 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}) { updateSelectedAppointmentDate({required int dateIndex, required int scheduleIndex}) {
@ -563,7 +614,7 @@ class AppointmentsVM extends BaseVM {
int serviceIndex = servicesInCurrentAppointment.indexWhere((element) => element.serviceId == currentServiceSelection!.serviceId!); int serviceIndex = servicesInCurrentAppointment.indexWhere((element) => element.serviceId == currentServiceSelection!.serviceId!);
serviceItemsFromApi[index].isUpdateOrSelected = selected; serviceItemsFromApi[index].isUpdateOrSelected = selected;
serviceItemsFromApi[index].isHomeSelected = isHomeTapped; serviceItemsFromApi[index].isHomeSelected = currentServiceSelection?.isHomeSelected ?? isHomeTapped;
log(" serviceIndex: $serviceIndex"); log(" serviceIndex: $serviceIndex");
if (selected) { if (selected) {
@ -709,13 +760,14 @@ class AppointmentsVM extends BaseVM {
Column( Column(
children: List.generate( children: List.generate(
selectedService.serviceItems!.length, selectedService.serviceItems!.length,
(index) => Row( (index) =>
mainAxisAlignment: MainAxisAlignment.spaceBetween, Row(
children: [ mainAxisAlignment: MainAxisAlignment.spaceBetween,
"${selectedService.serviceItems![index].name}".toText(fontSize: 12, color: MyColors.lightTextColor, isBold: true), children: [
"${selectedService.serviceItems![index].price} ${LocaleKeys.sar.tr()}".toText(fontSize: 12, isBold: true), "${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( Row(
@ -934,7 +986,10 @@ class AppointmentsVM extends BaseVM {
return InfoBottomSheet( return InfoBottomSheet(
title: LocaleKeys.reportComplaint.tr().toText(fontSize: 28, isBold: true, letterSpacing: -1.44), title: LocaleKeys.reportComplaint.tr().toText(fontSize: 28, isBold: true, letterSpacing: -1.44),
description: Padding( description: Padding(
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), padding: EdgeInsets.only(bottom: MediaQuery
.of(context)
.viewInsets
.bottom),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [

@ -651,8 +651,12 @@ class ServiceVM extends BaseVM {
} }
} }
Future cannotDeactivateServiceOrCategoryBottomSheet( Future cannotDeactivateServiceOrCategoryBottomSheet({
{required BuildContext mainContext, required List<AppointmentBasicDetailsModel> appointments, required String branchName, required bool isService}) async { required BuildContext mainContext,
required List<AppointmentBasicDetailsModel> appointments,
required String branchName,
required bool isService,
}) async {
return actionConfirmationBottomSheet( return actionConfirmationBottomSheet(
isOnlyOneButton: true, isOnlyOneButton: true,
context: mainContext, context: mainContext,
@ -666,21 +670,20 @@ class ServiceVM extends BaseVM {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// "${appointmentBasicDetailsModel.customerID}".toText(fontSize: 16, letterSpacing: -0.64),
showItem("${LocaleKeys.branchName.tr()}:", branchName),
if (appointmentBasicDetailsModel.appointmentStatusText != null && appointmentBasicDetailsModel.appointmentStatusText!.isNotEmpty) ...[ 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.date.tr()}:", "${appointmentBasicDetailsModel.slotDate}"),
showItem("${LocaleKeys.time.tr()}:", "${appointmentBasicDetailsModel.startTime} - ${appointmentBasicDetailsModel.startTime}"), showItem("${LocaleKeys.time.tr()}:", "${appointmentBasicDetailsModel.startTime} - ${appointmentBasicDetailsModel.endTime}"),
], ],
).toContainer(isShadowEnabled: true); ).toContainer(isShadowEnabled: true);
}), }),
actionButtonYes: Expanded( actionButtonYes: Expanded(
child: ShowFillButton( child: ShowFillButton(
maxHeight: 55, maxHeight: 55,
title: LocaleKeys.done.tr(), title: LocaleKeys.ok.tr(),
fontSize: 15, fontSize: 15,
onPressed: () => Navigator.pop(mainContext), onPressed: () => Navigator.pop(mainContext),
), ),
@ -688,7 +691,7 @@ class ServiceVM extends BaseVM {
actionButtonNo: Expanded( actionButtonNo: Expanded(
child: ShowFillButton( child: ShowFillButton(
maxHeight: 55, maxHeight: 55,
title: LocaleKeys.done.tr(), title: LocaleKeys.ok.tr(),
fontSize: 15, fontSize: 15,
onPressed: () => Navigator.pop(mainContext), onPressed: () => Navigator.pop(mainContext),
), ),

@ -148,11 +148,18 @@ class SubscriptionsVM extends BaseVM {
} }
String _convertDaysToMonths(int days) { String _convertDaysToMonths(int days) {
final int months = days ~/ 30; const int daysPerMonth = 30;
final int remainingDays = days % 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 ? ' & ' : ''}' : ''; String result = months > 0 ? '$months Month${months > 1 ? 's' : ''}${remainingDays > 0 ? ' & ' : ''}' : '';
result += remainingDays > 0 ? '$remainingDays Day${remainingDays > 1 ? 's' : ''}' : ''; result += remainingDays > 0 ? '$remainingDays Day${remainingDays > 1 ? 's' : ''}' : '';
if (result.isEmpty) result = '$days Day${days > 1 ? 's' : ''}';
return result; return result;
} }

@ -1,6 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'dart:developer'; import 'dart:developer';
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:mc_common_app/classes/app_state.dart'; import 'package:mc_common_app/classes/app_state.dart';
import 'package:mc_common_app/classes/consts.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/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/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_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/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/views/advertisement/components/ads_images_corousel_widget.dart';
import 'package:mc_common_app/widgets/bottom_sheet.dart'; import 'package:mc_common_app/widgets/bottom_sheet.dart';
@ -106,8 +106,10 @@ class _AdsDetailViewState extends State<AdsDetailView> {
child: Row( child: Row(
children: [ children: [
ShareAdButton(adDetails: widget.adDetails), ShareAdButton(adDetails: widget.adDetails),
8.width, if (!(widget.adDetails.isMyAd ?? false) && widget.adDetails.adPostStatus == AdPostStatus.active) ...[
FavoriteAdButton(adDetails: widget.adDetails), 8.width,
FavoriteAdButton(adDetails: widget.adDetails),
],
], ],
), ),
), ),
@ -352,6 +354,25 @@ class _AdsDetailViewState extends State<AdsDetailView> {
}).toWhiteContainer(width: double.infinity, allPading: 12); }).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<void> onExtendAdPressed() async { Future<void> onExtendAdPressed() async {
final AdVM adVM = context.read<AdVM>(); final AdVM adVM = context.read<AdVM>();
return actionConfirmationBottomSheet( return actionConfirmationBottomSheet(
@ -535,6 +556,10 @@ class _AdsDetailViewState extends State<AdsDetailView> {
if (widget.adDetails.vehicle!.damagereport != null && widget.adDetails.vehicle!.damagereport!.isNotEmpty) ...[ if (widget.adDetails.vehicle!.damagereport != null && widget.adDetails.vehicle!.damagereport!.isNotEmpty) ...[
buildDamagePartDetails(), buildDamagePartDetails(),
], ],
if ((widget.adDetails.qrCodePath ?? "").isNotEmpty) ...[
12.height,
buildQrCodeSection(),
],
if ((!(widget.adDetails.isMyAd ?? false) && (widget.adDetails.showContactDetail ?? false))) ...[ if ((!(widget.adDetails.isMyAd ?? false) && (widget.adDetails.showContactDetail ?? false))) ...[
12.height, 12.height,
buildPersonalInformationCard(context: context), buildPersonalInformationCard(context: context),
@ -714,18 +739,9 @@ class ShareAdButton extends StatelessWidget {
} }
shareMessage += '\n'; shareMessage += '\n';
// App download links // Dynamic ad link - opens directly in the app if installed, otherwise
shareMessage += '${LocaleKeys.downloadApp.tr()}\n'; // falls back to the store via the hosted web page for this path.
shareMessage += 'https://ms.hmg.com/ad/${adDetails.id}\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';
}
// Share using share_plus // Share using share_plus
Share.share(shareMessage); Share.share(shareMessage);

@ -109,7 +109,7 @@ class BuildAdDetailsActionButtonForExploreAds extends StatelessWidget {
], ],
), ),
const Divider(), const Divider(),
LocaleKeys.specialService.toText(fontSize: 16, isBold: true), LocaleKeys.specialService.tr().toText(fontSize: 16, isBold: true),
5.height, 5.height,
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
@ -245,8 +245,8 @@ class BuildAdDetailsActionButtonForExploreAds extends StatelessWidget {
} }
Widget defaultActionForProviderAndCustomer(BuildContext context, AdDetailsModel adDetailsModel) { Widget defaultActionForProviderAndCustomer(BuildContext context, AdDetailsModel adDetailsModel) {
return (adDetailsModel.phoneNo != null && adDetailsModel.phoneNo!.isNotEmpty) return (adDetailsModel.showContactDetail ?? false)
? Row( ? Row(
children: [ children: [
Expanded( Expanded(
child: ShowFillButton( child: ShowFillButton(

@ -190,6 +190,28 @@ class _AdsFilterViewState extends State<AdsFilterView> {
onHistoryItemDeleted: (index) => adVM.removeVehicleBrandsAdSearchHistory(index: index), onHistoryItemDeleted: (index) => adVM.removeVehicleBrandsAdSearchHistory(index: index),
onHistoryItemTapped: (DropValue value) => null, 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<DropValue> 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), const Divider(thickness: 1.2).paddingOnly(top: 7, bottom: 7),
SearchEntityWidget( SearchEntityWidget(
title: LocaleKeys.searchByVehicleYear.tr(), title: LocaleKeys.searchByVehicleYear.tr(),

@ -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();
}
}

@ -28,6 +28,7 @@ class AdsListWidget extends StatelessWidget {
final bool hasMoreData; final bool hasMoreData;
final bool isDraftAds; final bool isDraftAds;
final Function()? onFetchMoreAds; final Function()? onFetchMoreAds;
final Function()? onReturnFromAdDetail;
const AdsListWidget({ const AdsListWidget({
super.key, super.key,
@ -36,6 +37,7 @@ class AdsListWidget extends StatelessWidget {
required this.hasMoreData, required this.hasMoreData,
required this.isDraftAds, required this.isDraftAds,
this.onFetchMoreAds, this.onFetchMoreAds,
this.onReturnFromAdDetail,
this.scrollPhysics, this.scrollPhysics,
this.isAdsFragment = false, this.isAdsFragment = false,
}); });
@ -97,7 +99,9 @@ class AdsListWidget extends StatelessWidget {
isForDraft: true, isForDraft: true,
); );
} else { } 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)); ).toViewOnly(context, onTap: () => navigateWithName(context, AppRoutes.loginWithPassword, arguments: false));

@ -1,7 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:mc_common_app/generated/locale_keys.g.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/view_models/ad_view_model.dart'; import 'package:mc_common_app/view_models/ad_view_model.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:mc_common_app/extensions/int_extensions.dart'; import 'package:mc_common_app/extensions/int_extensions.dart';
@ -28,11 +27,19 @@ class _FavouriteAdsViewState extends State<FavouriteAdsView> {
void initState() { void initState() {
scheduleMicrotask(() async { scheduleMicrotask(() async {
final adVM = context.read<AdVM>(); final adVM = context.read<AdVM>();
await adVM.getFavouriteAds();
// Load filter options if not already loaded // Load filter options if not already loaded
if (adVM.exploreAdsFilterOptions.isEmpty) { if (adVM.exploreAdsFilterOptions.isEmpty) {
await adVM.populateAdsFilterList(); 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(); super.initState();
} }
@ -69,7 +76,7 @@ class _FavouriteAdsViewState extends State<FavouriteAdsView> {
filter.isSelected = false; filter.isSelected = false;
} }
adVM.exploreAdsFilterOptions[index].isSelected = true; adVM.exploreAdsFilterOptions[index].isSelected = true;
// Call API with filter (0 = all brands) // Call API with filter (0 = all brands)
await adVM.getFavouriteAds( await adVM.getFavouriteAds(
vehicleBrandId: selectedFilterId == 0 ? null : selectedFilterId, vehicleBrandId: selectedFilterId == 0 ? null : selectedFilterId,
@ -99,8 +106,12 @@ class _FavouriteAdsViewState extends State<FavouriteAdsView> {
isAdsFragment: false, isAdsFragment: false,
isDraftAds: false, isDraftAds: false,
shouldShowAdStatus: false, shouldShowAdStatus: false,
adsList: adVM.favouriteAds, // Use API-filtered list directly adsList: adVM.favouriteAds,
// Use API-filtered list directly
hasMoreData: false, hasMoreData: false,
onReturnFromAdDetail: () => adVM.getFavouriteAds(
vehicleBrandId: selectedBrandId == 0 ? null : selectedBrandId,
),
), ),
), ),
), ),

@ -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/int_extensions.dart';
import 'package:mc_common_app/extensions/string_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/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/item_model.dart';
import 'package:mc_common_app/models/services_models/service_model.dart'; import 'package:mc_common_app/models/services_models/service_model.dart';
import 'package:mc_common_app/theme/colors.dart'; import 'package:mc_common_app/theme/colors.dart';
@ -64,13 +63,7 @@ class BookAppointmentServicesView extends StatelessWidget {
Row( Row(
children: [ children: [
MyAssets.icEdit.buildSvg(height: 17, width: 17).onPress(() { MyAssets.icEdit.buildSvg(height: 17, width: 17).onPress(() {
appointmentsVM.updateBranchServiceId( appointmentsVM.editServiceInCurrentAppointment(serviceData);
SelectionModel(
selectedId: serviceData.serviceProviderServiceId!,
selectedOption: serviceData.serviceDescription ?? "",
itemPrice: "",
),
);
appointmentsVM.getServiceItems(serviceId: serviceData.serviceProviderServiceId!); appointmentsVM.getServiceItems(serviceId: serviceData.serviceProviderServiceId!);
navigateWithName(context, AppRoutes.bookAppointmentsItemView); navigateWithName(context, AppRoutes.bookAppointmentsItemView);
}), }),
@ -110,8 +103,7 @@ class BookAppointmentServicesView extends StatelessWidget {
Builder(builder: (context) { Builder(builder: (context) {
double totalKms = double.parse(serviceData.servicelocationInfo.distanceToBranch.toStringAsFixed(2)); double totalKms = double.parse(serviceData.servicelocationInfo.distanceToBranch.toStringAsFixed(2));
double pricePerKm = double.parse(serviceData.servicelocationInfo.homeChargesInCurrentService.toStringAsFixed(2)); double pricePerKm = double.parse(serviceData.servicelocationInfo.homeChargesInCurrentService.toStringAsFixed(2));
return ((serviceData.currentTotalServicePrice + (pricePerKm * totalKms)).toStringAsFixed(2)) return ((serviceData.currentTotalServicePrice + (pricePerKm * totalKms)).toStringAsFixed(2)).toText(fontSize: 32, isBold: true);
.toText(fontSize: 32, isBold: true);
}), }),
] else ...[ ] else ...[
((serviceData.currentTotalServicePrice).toString()).toText(fontSize: 32, isBold: true), ((serviceData.currentTotalServicePrice).toString()).toText(fontSize: 32, isBold: true),
@ -124,8 +116,8 @@ class BookAppointmentServicesView extends StatelessWidget {
], ],
], ],
).toWhiteContainer( ).toWhiteContainer(
width: double.infinity, width: double.infinity,
allPading: 12, allPading: 12,
margin: const EdgeInsets.symmetric(horizontal: 21, vertical: 10), margin: const EdgeInsets.symmetric(horizontal: 21, vertical: 10),
onTap: () => appointmentsVM.priceBreakDownClicked(context, serviceData), onTap: () => appointmentsVM.priceBreakDownClicked(context, serviceData),
); );

@ -1,3 +1,5 @@
import 'dart:developer';
import 'package:carousel_slider/carousel_slider.dart'; import 'package:carousel_slider/carousel_slider.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -168,7 +170,8 @@ class AppointmentHomeTileWidget extends StatelessWidget {
height: 19 / 12, height: 19 / 12,
) )
: const SizedBox(), : 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( Row(
children: [ children: [
!isForProvider ? MyAssets.miniClock.buildSvg(height: 12) : const SizedBox(), !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), (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( Row(
children: [ children: [
MyAssets.miniClock.buildSvg(height: 12), MyAssets.miniClock.buildSvg(height: 12),
2.width, 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, 5.height,

@ -38,6 +38,7 @@ class AdsFragment extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isArabic = AppState().isArabic(context);
return Consumer( return Consumer(
builder: (BuildContext context, AdVM adVM, Widget? child) { builder: (BuildContext context, AdVM adVM, Widget? child) {
return Scaffold( return Scaffold(
@ -46,8 +47,7 @@ class AdsFragment extends StatelessWidget {
isRemoveBackButton: true, isRemoveBackButton: true,
actions: [ actions: [
Padding( Padding(
padding: EdgeInsets.only( padding: EdgeInsets.only(top: adVM.adsFiltersCounter > 0 ? 20 : 0, right: isArabic ? 0 : 16, left: isArabic ? 16 : 0),
top: adVM.adsFiltersCounter > 0 ? 20 : 0, right: 21),
child: Badge( child: Badge(
isLabelVisible: adVM.adsFiltersCounter > 0, isLabelVisible: adVM.adsFiltersCounter > 0,
largeSize: 15, largeSize: 15,
@ -61,8 +61,7 @@ class AdsFragment extends StatelessWidget {
}) })
], ],
).toViewOnly(context, onTap: () { ).toViewOnly(context, onTap: () {
navigateWithName(context, AppRoutes.loginWithPassword, navigateWithName(context, AppRoutes.loginWithPassword, arguments: false);
arguments: false);
}), }),
body: Stack( body: Stack(
children: [ children: [
@ -73,8 +72,7 @@ class AdsFragment extends StatelessWidget {
children: [ children: [
16.height, 16.height,
Consumer( Consumer(
builder: builder: (BuildContext context, AdVM adVM, Widget? child) {
(BuildContext context, AdVM adVM, Widget? child) {
return Column( return Column(
children: [ children: [
Row( Row(
@ -85,9 +83,7 @@ class AdsFragment extends StatelessWidget {
maxHeight: 55, maxHeight: 55,
fontSize: 15, fontSize: 15,
title: LocaleKeys.exploreAds.tr(), title: LocaleKeys.exploreAds.tr(),
txtColor: adVM.isExploreAdsTapped txtColor: adVM.isExploreAdsTapped ? MyColors.white : MyColors.darkTextColor,
? MyColors.white
: MyColors.darkTextColor,
onPressed: () { onPressed: () {
log("accessToken: ${AppState().getUser.data!.accessToken}"); log("accessToken: ${AppState().getUser.data!.accessToken}");
log("AppState().getUser.data!.userInfo!.userId;: ${AppState().getUser.data!.userInfo!.userId}"); log("AppState().getUser.data!.userInfo!.userId;: ${AppState().getUser.data!.userInfo!.userId}");
@ -103,9 +99,7 @@ class AdsFragment extends StatelessWidget {
Expanded( Expanded(
child: ShowFillButton( child: ShowFillButton(
isFilled: !adVM.isExploreAdsTapped, isFilled: !adVM.isExploreAdsTapped,
txtColor: !adVM.isExploreAdsTapped txtColor: !adVM.isExploreAdsTapped ? MyColors.white : MyColors.darkTextColor,
? MyColors.white
: MyColors.darkTextColor,
maxHeight: 55, maxHeight: 55,
fontSize: 15, fontSize: 15,
title: LocaleKeys.myAds.tr(), title: LocaleKeys.myAds.tr(),
@ -113,9 +107,7 @@ class AdsFragment extends StatelessWidget {
adVM.updateIsExploreAds(false); adVM.updateIsExploreAds(false);
}, },
).toViewOnly(context, onTap: () { ).toViewOnly(context, onTap: () {
navigateWithName( navigateWithName(context, AppRoutes.loginWithPassword, arguments: false);
context, AppRoutes.loginWithPassword,
arguments: false);
}), }),
), ),
], ],
@ -127,8 +119,7 @@ class AdsFragment extends StatelessWidget {
showImages: true, showImages: true,
filterList: adVM.exploreAdsFilterOptions, filterList: adVM.exploreAdsFilterOptions,
onFilterTapped: (index, selectedFilterId) { onFilterTapped: (index, selectedFilterId) {
adVM.applyFilterOnExploreAds( adVM.applyFilterOnExploreAds(vehicleBrandId: selectedFilterId);
vehicleBrandId: selectedFilterId);
}, },
needLeftPadding: false, needLeftPadding: false,
).paddingOnly(left: 21), ).paddingOnly(left: 21),
@ -138,9 +129,7 @@ class AdsFragment extends StatelessWidget {
FiltersList( FiltersList(
filterList: adVM.myAdsFilterOptions, filterList: adVM.myAdsFilterOptions,
onFilterTapped: (index, selectedFilterId) { onFilterTapped: (index, selectedFilterId) {
adVM.applyFilterOnMyAds( adVM.applyFilterOnMyAds(adPostStatusEnum: selectedFilterId.toAdPostEnum());
adPostStatusEnum:
selectedFilterId.toAdPostEnum());
}, },
needLeftPadding: false, needLeftPadding: false,
).paddingOnly(left: 21), ).paddingOnly(left: 21),
@ -154,19 +143,11 @@ class AdsFragment extends StatelessWidget {
child: RefreshIndicator( child: RefreshIndicator(
onRefresh: () async { onRefresh: () async {
if (adVM.isExploreAdsTapped) { if (adVM.isExploreAdsTapped) {
int vehicleBrandId = adVM.exploreAdsFilterOptions int vehicleBrandId = adVM.exploreAdsFilterOptions.firstWhere((element) => element.isSelected).id;
.firstWhere((element) => element.isSelected) adVM.applyFilterOnExploreAds(vehicleBrandId: vehicleBrandId);
.id;
adVM.applyFilterOnExploreAds(
vehicleBrandId: vehicleBrandId);
} else { } else {
AdPostStatus adPostStatusEnum = adVM AdPostStatus adPostStatusEnum = adVM.myAdsFilterOptions.firstWhere((element) => element.isSelected).id.toAdPostEnum();
.myAdsFilterOptions adVM.applyFilterOnMyAds(adPostStatusEnum: adPostStatusEnum);
.firstWhere((element) => element.isSelected)
.id
.toAdPostEnum();
adVM.applyFilterOnMyAds(
adPostStatusEnum: adPostStatusEnum);
} }
}, },
child: adVM.state == ViewState.busy child: adVM.state == ViewState.busy
@ -176,22 +157,14 @@ class AdsFragment extends StatelessWidget {
isDraftAds: false, isDraftAds: false,
shouldShowAdStatus: !adVM.isExploreAdsTapped, shouldShowAdStatus: !adVM.isExploreAdsTapped,
adsList: getAdsList(adVM), adsList: getAdsList(adVM),
hasMoreData: adVM.isExploreAdsTapped hasMoreData: adVM.isExploreAdsTapped ? adVM.hasMoreDataForExploreAds : adVM.hasMoreDataForMyAds,
? adVM.hasMoreDataForExploreAds
: adVM.hasMoreDataForMyAds,
onFetchMoreAds: () async { onFetchMoreAds: () async {
AdPostStatus adPostStatusEnum = adVM AdPostStatus adPostStatusEnum = adVM.myAdsFilterOptions.firstWhere((element) => element.isSelected).id.toAdPostEnum();
.myAdsFilterOptions
.firstWhere(
(element) => element.isSelected)
.id
.toAdPostEnum();
if (adVM.isExploreAdsTapped) { if (adVM.isExploreAdsTapped) {
await adVM.fetchMoreExploreAds(); await adVM.fetchMoreExploreAds();
} else { } else {
await adVM.fetchMoreMyAds( await adVM.fetchMoreMyAds(adsStatus: adPostStatusEnum);
adsStatus: adPostStatusEnum);
} }
}, },
), ),
@ -215,8 +188,7 @@ class AdsFragment extends StatelessWidget {
color: Colors.black.withOpacity(0.3), color: Colors.black.withOpacity(0.3),
child: const Center( child: const Center(
child: CircularProgressIndicator( child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>( valueColor: AlwaysStoppedAnimation<Color>(MyColors.darkPrimaryColor),
MyColors.darkPrimaryColor),
), ),
), ),
), ),
@ -233,31 +205,23 @@ class AdsFragment extends StatelessWidget {
} }
if (AppState().userType != UserType.providerDealer) { if (AppState().userType != UserType.providerDealer) {
if (adVM.vehicleAdDurationId.selectedId == -1 && if (adVM.vehicleAdDurationId.selectedId == -1 && adVM.vehicleAdsDurations.isNotEmpty) {
adVM.vehicleAdsDurations.isNotEmpty) {
SelectionModel selection = SelectionModel( SelectionModel selection = SelectionModel(
selectedId: adVM.vehicleAdsDurations.first.id ?? 0, selectedId: adVM.vehicleAdsDurations.first.id ?? 0,
selectedOption: selectedOption: ("${adVM.vehicleAdsDurations.first.days} ${LocaleKeys.daysVar.tr()}"),
("${adVM.vehicleAdsDurations.first.days} ${LocaleKeys.daysVar.tr()}"),
itemPrice: adVM.vehicleAdsDurations.first.price!.toString(), itemPrice: adVM.vehicleAdsDurations.first.price!.toString(),
currency: currency: adVM.vehicleAdsDurations.first.currency!.toString(),
adVM.vehicleAdsDurations.first.currency!.toString(),
); );
adVM.updateVehicleAdDurationId(selection); adVM.updateVehicleAdDurationId(selection);
} }
} }
navigateWithName(context, AppRoutes.selectAdTypeView, arguments: [ navigateWithName(context, AppRoutes.selectAdTypeView, arguments: [injector.get<AppState>().currentAppType == AppType.provider, false, -1]);
injector.get<AppState>().currentAppType == AppType.provider,
false,
-1
]);
}, },
backgroundColor: MyColors.darkPrimaryColor, backgroundColor: MyColors.darkPrimaryColor,
child: const Icon(Icons.add, color: MyColors.white), child: const Icon(Icons.add, color: MyColors.white),
).toViewOnly(context, onTap: () { ).toViewOnly(context, onTap: () {
navigateWithName(context, AppRoutes.loginWithPassword, navigateWithName(context, AppRoutes.loginWithPassword, arguments: false);
arguments: false);
}), }),
); );
}, },

@ -26,6 +26,8 @@ class _UpdateUserDetailsState extends State<UpdateUserDetails> {
@override @override
void initState() { void initState() {
userVM = Provider.of<UserVM>(context, listen: false); userVM = Provider.of<UserVM>(context, listen: false);
firstname = AppState().getUser.data?.userInfo?.firstName ?? "";
lastname = AppState().getUser.data?.userInfo?.lastName ?? "";
super.initState(); super.initState();
} }
@ -44,11 +46,13 @@ class _UpdateUserDetailsState extends State<UpdateUserDetails> {
children: [ children: [
TxtField( TxtField(
hint: LocaleKeys.enterNewFirstName.tr(), hint: LocaleKeys.enterNewFirstName.tr(),
value: firstname,
onChanged: (v) => firstname = v, onChanged: (v) => firstname = v,
), ),
12.height, 12.height,
TxtField( TxtField(
hint: LocaleKeys.enterNewLastName.tr(), hint: LocaleKeys.enterNewLastName.tr(),
value: lastname,
onChanged: (v) => lastname = v, onChanged: (v) => lastname = v,
), ),
40.height, 40.height,

@ -48,6 +48,7 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isArabic = AppState().isArabic(context);
return Column( return Column(
children: [ children: [
AppBar( AppBar(
@ -101,7 +102,7 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
10.width, 10.width,
SvgPicture.asset(MyAssets.dashboardDrawerIcon), SvgPicture.asset(MyAssets.dashboardDrawerIcon),
], ],
).paddingOnly(left: 21), ).paddingOnly(left: isArabic ? 0 : 21, right: isArabic ? 21 : 0),
) )
: isRemoveBackButton : isRemoveBackButton
? null ? null
@ -110,7 +111,7 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
21.width, 21.width,
const Icon(Icons.arrow_back_ios, color: Colors.black, size: 16) const Icon(Icons.arrow_back_ios, color: Colors.black, size: 16)
.toContainer( .toContainer(
padding: const EdgeInsets.only(left: 5), padding: EdgeInsets.only(left: isArabic ? 0 : 5, right: isArabic ? 5 : 0),
borderRadius: 100, borderRadius: 100,
borderColor: MyColors.lightGreyEFColor, borderColor: MyColors.lightGreyEFColor,
isEnabledBorder: true, isEnabledBorder: true,

@ -1,8 +1,10 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:mc_common_app/classes/consts.dart'; import 'package:mc_common_app/classes/consts.dart';
import 'package:mc_common_app/extensions/int_extensions.dart'; import 'package:mc_common_app/extensions/int_extensions.dart';
import 'package:mc_common_app/extensions/string_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/theme/colors.dart';
import 'package:mc_common_app/utils/utils.dart'; import 'package:mc_common_app/utils/utils.dart';
import 'package:mc_common_app/widgets/extensions/extensions_widget.dart'; import 'package:mc_common_app/widgets/extensions/extensions_widget.dart';
@ -89,45 +91,47 @@ class _DropdownFieldState extends State<DropdownField> {
) )
else else
// Show normal dropdown when selectable // Show normal dropdown when selectable
Container( Builder(builder: (context) {
decoration: widget.showAppointmentPickerVariant ? null : Utils.containerColorRadiusBorderWidth(MyColors.white, 0, MyColors.darkPrimaryColor, 2), // Distinguish "no list passed" (use placeholder defaultV) from "list explicitly empty"
margin: const EdgeInsets.all(0), // (real data fetched but nothing available) so we can show a meaningful empty-state.
padding: const EdgeInsets.only(left: 8, right: 8), final List<DropValue> items = widget.list ?? defaultV;
width: widget.showAppointmentPickerVariant ? 170 : null, final bool hasNoData = widget.list != null && widget.list!.isEmpty;
child: DropdownButton<DropValue>( return InkWell(
value: dropdownValue, onTap: hasNoData
icon: const Icon( ? null
Icons.keyboard_arrow_down_sharp, : () {
size: 21, widget.onTap?.call();
), _openSearchablePicker(context, items);
elevation: 16, },
iconSize: widget.showAppointmentPickerVariant ? 26 : 16, child: Container(
iconEnabledColor: borderColor, decoration: widget.showAppointmentPickerVariant ? null : Utils.containerColorRadiusBorderWidth(MyColors.white, 0, MyColors.darkPrimaryColor, 2),
iconDisabledColor: borderColor, margin: const EdgeInsets.all(0),
isExpanded: true, padding: const EdgeInsets.only(left: 8, right: 8, top: 2, bottom: 2),
style: const TextStyle( width: widget.showAppointmentPickerVariant ? 170 : null,
color: borderColor, child: Row(
fontSize: 15, 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<DropdownMenuItem<DropValue>>(
(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),
);
},
).toList(),
),
),
if (widget.errorValue != "") if (widget.errorValue != "")
Row( Row(
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
@ -138,4 +142,166 @@ class _DropdownFieldState extends State<DropdownField> {
], ],
); );
} }
void _openSearchablePicker(BuildContext context, List<DropValue> 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<DropValue> 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<DropValue> 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,
],
),
),
),
),
);
}
} }

@ -9,6 +9,38 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.41" 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: args:
dependency: transitive dependency: transitive
description: description:
@ -653,6 +685,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.5.12" version: "0.5.12"
gtk:
dependency: transitive
description:
name: gtk
sha256: "4ff85b2a16724029dd9e5bbb5a94b6918f9973f74ba571c949d2002801879cf5"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
hexcolor: hexcolor:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1539,5 +1579,5 @@ packages:
source: hosted source: hosted
version: "6.5.0" version: "6.5.0"
sdks: sdks:
dart: ">=3.10.0-0 <4.0.0" dart: ">=3.12.0 <4.0.0"
flutter: ">=3.35.0" flutter: ">=3.44.0"

@ -64,6 +64,7 @@ dependencies:
local_auth: ^2.2.0 local_auth: ^2.2.0
# huawei_fido: ^6.3.0+305 # huawei_fido: ^6.3.0+305
device_info_plus: ^10.1.0 device_info_plus: ^10.1.0
app_links: ^7.2.1
dev_dependencies: dev_dependencies:

Loading…
Cancel
Save