Compare commits

..

No commits in common. 'master' and 'fatima_dev' have entirely different histories.

@ -338,7 +338,6 @@
"qrCode": "رمز الاستجابة السريعة",
"downgradeAppliedAfterExpiry": "سيتم تطبيق هذه الباقة تلقائيًا بعد انتهاء صلاحية باقتك الحالية.",
"downgradeDeactivateInstruction": "لتحديد هذا الاشتراك، يجب عليك إلغاء تفعيل الفروع والمستخدمين حيث أن هذه الباقة تدعم فقط {branches} و {users}. يمكنك إعادة تفعيل هذه الفروع لاحقًا بعد ترقية الباقة",
"adChargeDueToSubscriptionNotice": "انتهت صلاحية باقتك أو لم يتبق لديك أي إعلانات في باقتك الحالية. سيتم محاسبتك بناءً على المدة والسعر الذي تختاره أدناه.",
"vehicleDamagePart": "جزء المركبة المتضرر",
"duration": "المدة",
"specialServices": "الخدمات الخاصة",
@ -826,7 +825,6 @@
"noOfInvites": "عدد الدعوات",
"noSpecialServicesAvailable": "لا توجد خدمات خاصة متاحة للمنطقة المحددة.",
"customService": "الخدمة المخصصة",
"lookingToAddCustomService": "هل تريد إضافة خدمة مخصصة؟",
"selectServiceDetails": "اختر تفاصيل الخدمة",
"addServiceDetails": "أضف تفاصيل الخدمة",
"companyNameMandatory": "اسم الشركة إلزامي",

@ -341,7 +341,6 @@
"qrCode": "QR Code",
"downgradeAppliedAfterExpiry": "This package will be applied automatically after your current package expires.",
"downgradeDeactivateInstruction": "To select this subscription you need to deactivate your branches and users as this package only supports {branches} and {users}. You can reactivate these branches later after upgrading the package",
"adChargeDueToSubscriptionNotice": "Your subscription has expired or you have no ads remaining in your current plan. You'll be charged based on the duration and price you select below.",
"vehicleDamagePart": "Vehicle Damage Part",
"duration": "Duration",
"specialServices": "Special Services",
@ -829,7 +828,6 @@
"noOfInvites": "No of invites",
"noSpecialServicesAvailable": "There are no special services available for the selected region.",
"customService": "Custom Service",
"lookingToAddCustomService": "Looking to add a custom service?",
"selectServiceDetails": "Select service details",
"addServiceDetails": "Add service details",
"connectionProblem": "Connection Problem",

@ -77,29 +77,6 @@ class AppState {
_providerSubscription = value;
}
// A notification tapped before login completed (cold start, expired session) --
// stashed by FirebaseMessagingService so it can be resumed once login finishes.
// Must be cleared on logout, same as setUser/setproviderSubscription below -- otherwise
// a pending notification from the previous session could fire for the next user who
// logs in on this device.
Map<String, dynamic>? _pendingNotificationData;
Map<String, dynamic>? get getPendingNotificationData => _pendingNotificationData;
set setPendingNotificationData(Map<String, dynamic>? value) {
_pendingNotificationData = value;
}
// getproviderSubscription falls back to a dummy Subscription(name: "") when there's no
// real data, so adsRemaining != null is the signal that this is actual subscription data,
// not the placeholder.
// Ads-specific on purpose: only checks adsRemaining, not branchesRemaining/subUsersRemaining.
// Don't reuse this for branch/user-limit checks -- add a separate getter for those instead.
bool get hasAdsRemainingInActiveSubscription {
final subscription = getproviderSubscription.first;
return subscription.adsRemaining != null && subscription.isExpired != true && (subscription.adsRemaining ?? 0) > 0;
}
DropValue? _userRegisterCountrySelection;
DropValue get getUserRegisterCountrySelection =>

@ -90,6 +90,7 @@ class AppRoutes {
static const String appointment = "/appointment";
static const String appointmentDetailList = "/appointmentDetailList";
static const String updateAppointmentPage = "/updateAppointmentPage";
static const String addServiceInAppointment = "/addServiceInAppointment";
static const String mergeAppointments = "/mergeAppointments";
//Services

@ -339,7 +339,6 @@ class CodegenLoader extends AssetLoader {
"damagePartPictures": "صور الأجزاء المتضررة",
"qrCode": "رمز الاستجابة السريعة",
"downgradeAppliedAfterExpiry": "سيتم تطبيق هذه الباقة تلقائيًا بعد انتهاء صلاحية باقتك الحالية.",
"adChargeDueToSubscriptionNotice": "انتهت صلاحية باقتك أو لم يتبق لديك أي إعلانات في باقتك الحالية. سيتم محاسبتك بناءً على المدة والسعر الذي تختاره أدناه.",
"downgradeDeactivateInstruction":
"لتحديد هذا الاشتراك، يجب عليك إلغاء تفعيل الفروع والمستخدمين حيث أن هذه الباقة تدعم فقط {branches} و {users}. يمكنك إعادة تفعيل هذه الفروع لاحقًا بعد ترقية الباقة",
"vehicleDamagePart": "جزء المركبة المتضرر",
@ -830,7 +829,6 @@ class CodegenLoader extends AssetLoader {
"noOfInvites": "عدد الدعوات",
"noSpecialServicesAvailable": "لا توجد خدمات خاصة متاحة للمنطقة المحددة.",
"customService": "الخدمة المخصصة",
"lookingToAddCustomService": "هل تريد إضافة خدمة مخصصة؟",
"selectServiceDetails": "اختر تفاصيل الخدمة",
"addServiceDetails": "أضف تفاصيل الخدمة",
"companyNameMandatory": "اسم الشركة إلزامي",
@ -1278,7 +1276,6 @@ class CodegenLoader extends AssetLoader {
"damagePartPictures": "Damage Part Pictures",
"qrCode": "QR Code",
"downgradeAppliedAfterExpiry": "This package will be applied automatically after your current package expires.",
"adChargeDueToSubscriptionNotice": "Your subscription has expired or you have no ads remaining in your current plan. You'll be charged based on the duration and price you select below.",
"downgradeDeactivateInstruction":
"To select this subscription you need to deactivate your branches and users as this package only supports {branches} and {users}. You can reactivate these branches later after upgrading the package",
"vehicleDamagePart": "Vehicle Damage Part",
@ -1769,7 +1766,6 @@ class CodegenLoader extends AssetLoader {
"noOfInvites": "No of invites",
"noSpecialServicesAvailable": "There are no special services available for the selected region.",
"customService": "Custom Service",
"lookingToAddCustomService": "Looking to add a custom service?",
"selectServiceDetails": "Select service details",
"addServiceDetails": "Add service details",
"connectionProblem": "Connection Problem",

@ -319,7 +319,6 @@ abstract class LocaleKeys {
static const damagePartPictures = 'damagePartPictures';
static const qrCode = 'qrCode';
static const downgradeAppliedAfterExpiry = 'downgradeAppliedAfterExpiry';
static const adChargeDueToSubscriptionNotice = 'adChargeDueToSubscriptionNotice';
static const downgradeDeactivateInstruction = 'downgradeDeactivateInstruction';
static const vehicleDamagePart = 'vehicleDamagePart';
static const duration = 'duration';
@ -807,7 +806,6 @@ abstract class LocaleKeys {
static const noOfInvites = 'noOfInvites';
static const noSpecialServicesAvailable = 'noSpecialServicesAvailable';
static const customService = 'customService';
static const lookingToAddCustomService = 'lookingToAddCustomService';
static const selectServiceDetails = 'selectServiceDetails';
static const addServiceDetails = 'addServiceDetails';
static const companyNameMandatory = 'companyNameMandatory';

@ -202,9 +202,6 @@ class Vehicle {
Condition? condition;
Mileage? mileage;
Condition? model;
// Top-level brand id, independent of model -- lets a brand-only selection (no model
// picked yet) survive a draft reload instead of relying solely on model.vehicleBrandID.
int? vehicleBrandID;
ModelYear? modelyear;
Condition? sellertype;
Condition? transmission;
@ -239,7 +236,6 @@ class Vehicle {
this.condition,
this.mileage,
this.model,
this.vehicleBrandID,
this.modelyear,
this.sellertype,
this.transmission,
@ -274,7 +270,6 @@ class Vehicle {
condition = json['condition'] != null ? Condition.fromJson(json['condition']) : null;
mileage = json['mileage'] != null ? Mileage.fromJson(json['mileage']) : null;
model = json['model'] != null ? Condition.fromJson(json['model']) : null;
vehicleBrandID = json['vehicleBrandID'];
modelyear = json['modelyear'] != null ? ModelYear.fromJson(json['modelyear']) : null;
sellertype = json['sellertype'] != null ? Condition.fromJson(json['sellertype']) : null;
transmission = json['transmission'] != null ? Condition.fromJson(json['transmission']) : null;

@ -102,7 +102,6 @@ class VehiclePosting {
int? id;
String? userID;
int? vehicleType;
int? vehicleBrandID;
int? vehicleModelID;
int? vehicleModelYearID;
int? vehicleColorID;
@ -133,7 +132,6 @@ class VehiclePosting {
this.id,
this.userID,
this.vehicleType,
this.vehicleBrandID,
this.vehicleModelID,
this.vehicleModelYearID,
this.vehicleColorID,
@ -165,7 +163,6 @@ class VehiclePosting {
id = json['id'];
userID = json['userID'];
vehicleType = json['vehicleType'];
vehicleBrandID = json['vehicleBrandID'];
vehicleModelID = json['vehicleModelID'];
vehicleModelYearID = json['vehicleModelYearID'];
vehicleColorID = json['vehicleColorID'];
@ -205,7 +202,7 @@ class VehiclePosting {
@override
String toString() {
return 'VehiclePosting{id: $id, userID: $userID, vehicleType: $vehicleType, vehicleBrandID: $vehicleBrandID, vehicleModelID: $vehicleModelID, vehicleModelYearID: $vehicleModelYearID, vehicleColorID: $vehicleColorID, vehicleCategoryID: $vehicleCategoryID, vehicleConditionID: $vehicleConditionID, vehicleMileageID: $vehicleMileageID, vehicleTransmissionID: $vehicleTransmissionID, vehicleSellerTypeID: $vehicleSellerTypeID, cityID: $cityID, price: $price, vehicleVIN: $vehicleVIN, vehicleDescription: $vehicleDescription, vehicleTitle: $vehicleTitle, vehicleDescriptionN: $vehicleDescriptionN, isFinanceAvailable: $isFinanceAvailable, warantyYears: $warantyYears, demandAmount: $demandAmount, adStatus: $adStatus, vehiclePostingImages: $vehiclePostingImages, vehiclePostingDamageParts: $vehiclePostingDamageParts, phoneNo: $phoneNo, whatsAppNo: $whatsAppNo, odometer: $odometer, isInUsed: $isInUsed, isAccidentFree: $isAccidentFree}';
return 'VehiclePosting{id: $id, userID: $userID, vehicleType: $vehicleType, vehicleModelID: $vehicleModelID, vehicleModelYearID: $vehicleModelYearID, vehicleColorID: $vehicleColorID, vehicleCategoryID: $vehicleCategoryID, vehicleConditionID: $vehicleConditionID, vehicleMileageID: $vehicleMileageID, vehicleTransmissionID: $vehicleTransmissionID, vehicleSellerTypeID: $vehicleSellerTypeID, cityID: $cityID, price: $price, vehicleVIN: $vehicleVIN, vehicleDescription: $vehicleDescription, vehicleTitle: $vehicleTitle, vehicleDescriptionN: $vehicleDescriptionN, isFinanceAvailable: $isFinanceAvailable, warantyYears: $warantyYears, demandAmount: $demandAmount, adStatus: $adStatus, vehiclePostingImages: $vehiclePostingImages, vehiclePostingDamageParts: $vehiclePostingDamageParts, phoneNo: $phoneNo, whatsAppNo: $whatsAppNo, odometer: $odometer, isInUsed: $isInUsed, isAccidentFree: $isAccidentFree}';
}
}

@ -190,7 +190,6 @@ class AdsRepoImp implements AdsRepo {
"id": isCreateNew ? 0 : adsCreationPayloadModel.vehiclePosting!.id ?? 0,
"userID": adsCreationPayloadModel.vehiclePosting!.userID,
"vehicleType": adsCreationPayloadModel.vehiclePosting!.vehicleType,
"vehicleBrandID": adsCreationPayloadModel.vehiclePosting!.vehicleBrandID,
"vehicleModelID": adsCreationPayloadModel.vehiclePosting!.vehicleModelID,
"vehicleModelYearID": adsCreationPayloadModel.vehiclePosting!.vehicleModelYearID,
"vehicleColorID": adsCreationPayloadModel.vehiclePosting!.vehicleColorID,
@ -287,7 +286,6 @@ class AdsRepoImp implements AdsRepo {
"id": adsCreationPayloadModel.vehiclePosting!.id ?? 0,
"userID": adsCreationPayloadModel.vehiclePosting!.userID,
"vehicleType": adsCreationPayloadModel.vehiclePosting!.vehicleType,
"vehicleBrandID": adsCreationPayloadModel.vehiclePosting!.vehicleBrandID,
"vehicleModelID": adsCreationPayloadModel.vehiclePosting!.vehicleModelID,
"vehicleModelYearID": adsCreationPayloadModel.vehiclePosting!.vehicleModelYearID,
"vehicleColorID": adsCreationPayloadModel.vehiclePosting!.vehicleColorID,

@ -14,12 +14,7 @@ import 'package:mc_common_app/main.dart';
import 'package:mc_common_app/services/chat_navigation_service.dart';
import 'package:mc_common_app/utils/enums.dart';
import 'package:mc_common_app/utils/navigator.dart';
import 'package:mc_common_app/utils/utils.dart';
import 'package:mc_common_app/view_models/chat_view_model.dart';
import 'package:mc_common_app/view_models/dashboard_view_model_customer.dart';
import 'package:mc_common_app/view_models/dashboard_view_model_provider.dart';
import 'package:mc_common_app/models/requests_models/request_model.dart';
import 'package:mc_common_app/view_models/requests_view_model.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:provider/provider.dart';
@ -27,11 +22,6 @@ abstract class FirebaseMessagingService {
Future<String> getFirebaseToken();
Future<void> initializeNotifications();
// Replays a notification tap that arrived before the user was logged in (cold start,
// expired session, etc.) -- call this right after login/OTP success finishes
// navigating to the dashboard, so the original notification target isn't lost.
Future<void> resumePendingNotificationIfAny();
}
class FirebaseMessagingServiceImp implements FirebaseMessagingService {
@ -190,17 +180,6 @@ class FirebaseMessagingServiceImp implements FirebaseMessagingService {
_handleNotificationNavigation(message.data);
}
@override
Future<void> resumePendingNotificationIfAny() async {
final pendingData = AppState().getPendingNotificationData;
if (pendingData == null) return;
// Clear before replaying -- _handleNotificationNavigation re-stashes it again if the
// user still isn't authenticated for some reason, so this can't lose data either way.
AppState().setPendingNotificationData = null;
log("Resuming notification navigation that was pending login: $pendingData");
_handleNotificationNavigation(pendingData);
}
void _handleNotificationNavigation(Map<String, dynamic> data) async {
log("========== Handling Notification Navigation ==========");
log("Full data received: $data");
@ -211,33 +190,13 @@ class FirebaseMessagingServiceImp implements FirebaseMessagingService {
return;
}
// Not logged in yet (cold start landing on login, expired session, etc.) -- every
// navigation target below needs an authenticated VM call or a dashboard that only
// exists post-login, so there's nothing to navigate to right now. Stash it instead
// of silently dropping it; resumePendingNotificationIfAny() replays this exact call
// once login completes.
if ((AppState().getUser.data?.accessToken ?? "").isEmpty) {
log("User not authenticated yet, stashing notification data to resume after login");
AppState().setPendingNotificationData = data;
log("======================================================");
return;
}
// Backend sends NotiType as string. Full mapping per backend:
// 1 - Request Offer Chat
// 2 - Advertisement Chat
// 3 - General Chat
// 4 - Others
// 5 - Road Side
// 6 - Special car & spare parts requests
// Only 6 has a screen to land on (Requests tab); 4 and 5 are upcoming features with
// nowhere to navigate yet, so they're just logged and skipped.
// Backend sends NotiType as string: "1" = Request, "2" = Ads, "3" = General
String? notiTypeStr = data['NotiType'];
int? notiType = int.tryParse(notiTypeStr ?? '');
log("NotiType from backend: $notiTypeStr (parsed: $notiType)");
if (notiType == null || notiType < 1 || notiType > 6) {
if (notiType == null || notiType < 1 || notiType > 3) {
log("Invalid or missing NotiType, skipping navigation");
log("======================================================");
return;
@ -255,14 +214,6 @@ class FirebaseMessagingServiceImp implements FirebaseMessagingService {
case 3:
chatType = 'general_chat';
break;
case 4:
case 5:
// Others / Road Side -- upcoming features, no screen to navigate to yet.
chatType = 'upcoming_feature';
break;
case 6:
chatType = 'requests_tab';
break;
default:
log("Unknown NotiType: $notiType");
log("======================================================");
@ -296,17 +247,10 @@ class FirebaseMessagingServiceImp implements FirebaseMessagingService {
log("Navigating to Advertisement Chat...");
await _navigateToAdChat(context, data);
break;
case 'general_chat':
case 'general':
log("Navigating to General Chat...");
await _navigateToGeneralChat(context, data);
break;
case 'requests_tab':
log("Navigating to Requests tab...");
await _navigateToRequestsTab(context, data);
break;
case 'upcoming_feature':
log("NotiType $notiType (Others/Road Side) is for an upcoming feature -- not yet implemented, skipping navigation");
break;
default:
logger.e("Unknown chat type: $chatType");
}
@ -432,83 +376,6 @@ class FirebaseMessagingServiceImp implements FirebaseMessagingService {
}
}
// Requests tab is index 4 in both apps' fragment lists.
void _switchToRequestsTab(BuildContext context) {
if (AppState().currentAppType == AppType.customer) {
Provider.of<DashboardVmCustomer>(context, listen: false).onNavbarTapped(4);
} else {
Provider.of<DashboardVMProvider>(context, listen: false).onNavbarTapped(4);
}
}
Future<void> _navigateToRequestsTab(BuildContext context, Map<String, dynamic> data) async {
try {
// Switch tabs first so the Requests tab is already showing underneath by the time
// the loader (if any) comes down or the detail page opens -- matches what tapping
// the tab manually would leave behind.
_switchToRequestsTab(context);
// NotiType 4 (Others) and 5 (Road Side) land here with no RequestID to resolve --
// the Requests tab is as specific as we can get for them.
int requestId = int.tryParse(data['RequestID']?.toString() ?? data['request_id']?.toString() ?? '0') ?? 0;
if (requestId == 0) {
log("No RequestID in payload, just switching to Requests tab");
return;
}
final requestsVM = Provider.of<RequestsVM>(context, listen: false);
int requestIndex = requestsVM.myFilteredRequests.indexWhere((request) => request.id == requestId);
if (requestIndex == -1) {
// Cold start, or the wrong request-type filter is active. Backend doesn't send
// requestType in the notification payload yet (asked, pending) -- until then,
// guess in this specific order: spare parts first, then special cars, then
// others, stopping as soon as the request turns up. Replace this loop with a
// direct filter lookup once backend adds requestType to the payload.
// This can take a few seconds (up to 3 sequential network calls) -- show a
// full-screen loader so the tap doesn't look like it did nothing.
Utils.showLoading(context);
try {
const searchOrder = [RequestsTypeEnum.serviceRequest, RequestsTypeEnum.specialCarRequest, RequestsTypeEnum.other];
for (final requestsTypeEnum in searchOrder) {
await requestsVM.applyFilterOnRequestsVM(requestsTypeEnum: requestsTypeEnum);
requestIndex = requestsVM.myFilteredRequests.indexWhere((request) => request.id == requestId);
if (requestIndex != -1) break;
}
} finally {
Utils.hideLoading(context);
}
}
if (requestIndex == -1) {
log("⚠️ Request $requestId not found after refresh, falling back to Requests tab");
return;
}
RequestModel request = requestsVM.myFilteredRequests[requestIndex];
// Same gate as the manual tap in requests_fragment.dart -- these statuses have
// nothing to show on the detail page, so land on the tab instead.
if (request.requestStatus == RequestStatusEnum.pending || request.requestStatus == RequestStatusEnum.cancelled || request.requestStatus == RequestStatusEnum.expired) {
log("Request $requestId status (${request.requestStatus}) has no detail view, falling back to Requests tab");
return;
}
requestsVM.updateCurrentSelectedRequest(request);
if (AppState().currentAppType == AppType.customer) {
log("✅ Navigating to offers list for request: $requestId");
navigateWithName(context, AppRoutes.offersListPage, arguments: request.id);
} else {
log("✅ Navigating to request detail for request: $requestId");
RequestDetailPageArguments requestDetailPageArguments = RequestDetailPageArguments(requestIndex: requestIndex, requestModel: request);
navigateWithName(context, AppRoutes.requestsDetailPage, arguments: requestDetailPageArguments);
}
} catch (e) {
logger.e("Error navigating to request detail: $e");
}
}
Future<void> displayNotification({
required String title,
required String body,

@ -24,10 +24,10 @@ import 'package:mc_common_app/models/advertisment_models/ad_details_model.dart';
import 'package:mc_common_app/theme/colors.dart';
import 'package:mc_common_app/utils/enums.dart';
import 'package:mc_common_app/widgets/loading_dialog.dart';
import 'package:mc_common_app/widgets/common_widgets/app_toast.dart';
import 'package:share_plus/share_plus.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'package:toastification/toastification.dart';
class Utils {
static bool _isLoadingVisible = false;
@ -44,20 +44,18 @@ class Utils {
return 'https://ms.hmg.com/ad/${adDetails.id}?app=$appParam';
}
// Neutral styling for now -- existing call sites across the app were never written
// with a success/error distinction in mind (confirmed: plenty of messageStatus == 1
// success branches call this same function as the error branches), so defaulting to
// red/green here would mislabel most of them until those ~340 call sites are reviewed
// and updated individually to pass isSuccess explicitly.
static void showToast(String message, {bool isSuccess = false, VoidCallback? onComplete}) {
AppToast.show(
message,
seconds: onComplete != null ? AppToast.actionSeconds : AppToast.plainSeconds,
showIcon: onComplete != null,
showCountdownText: true,
bgColor: isSuccess ? AppToast.successBgColor : null,
textColor: isSuccess ? AppToast.successTextColor : null,
onComplete: onComplete,
static void showToast(String message) {
toastification.show(
type: ToastificationType.info,
style: ToastificationStyle.minimal,
title: Text(
message,
maxLines: 5,
overflow: TextOverflow.ellipsis,
),
autoCloseDuration: const Duration(seconds: 3),
alignment: Alignment.topCenter,
showProgressBar: false,
);
}
@ -116,11 +114,7 @@ class Utils {
return "";
}
return ("${timeOfDay.hour
.toString()
.length == 1 ? "0" : ""}${timeOfDay.hour}:${timeOfDay.minute
.toString()
.length == 1 ? "0" : ""}${timeOfDay.minute}").toString();
return ("${timeOfDay.hour.toString().length == 1 ? "0" : ""}${timeOfDay.hour}:${timeOfDay.minute.toString().length == 1 ? "0" : ""}${timeOfDay.minute}").toString();
}
static dynamic getNotNullValue(List<dynamic> list, int index) {
@ -559,7 +553,7 @@ class Utils {
color: chipColor,
),
padding: padding,
child: text.toText(fontSize: 8, color: textColor, letterSpacing: -0.60));
child: text.toText(fontSize: 10, color: textColor, letterSpacing: -0.50));
}
static InputDecoration txtField(String label) {
@ -695,7 +689,7 @@ class Utils {
border: Border.all(
width: w, //
color: color // <--- border width here
),
),
borderRadius: BorderRadius.circular(radius),
);
}
@ -718,7 +712,7 @@ class Utils {
border: Border.all(
width: 1, //
color: color // <--- border width here
),
),
borderRadius: BorderRadius.circular(radius),
);
}
@ -736,8 +730,7 @@ class Utils {
}
static String getAdsPaymentBrowserForm({required int paymentId, required int adId}) {
return '<html> <head></head><body><form id="paymentForm" action="${ApiConsts
.paymentWebViewUrl}" method="post"><input type="hidden" name="PaymentType" value="$paymentId"><input type="hidden" name="AdsID" value="$adId"></form><script type="text/javascript"> document.getElementById("paymentForm").submit(); </script></body></html>';
return '<html> <head></head><body><form id="paymentForm" action="${ApiConsts.paymentWebViewUrl}" method="post"><input type="hidden" name="PaymentType" value="$paymentId"><input type="hidden" name="AdsID" value="$adId"></form><script type="text/javascript"> document.getElementById("paymentForm").submit(); </script></body></html>';
}
// BOTTOM SHEETS
@ -798,18 +791,18 @@ class Utils {
switch (inviteTypeEnum) {
case InviteTypeEnum.whatsapp:
// WhatsApp requires a phone number and message, format like this:
// WhatsApp requires a phone number and message, format like this:
url = 'https://wa.me/?text=${Uri.encodeComponent(message)}';
break;
case InviteTypeEnum.sms:
// SMS: 'sms:<phone_number>?body=<message>'
// You can add a phone number, or keep it empty to allow the user to input one.
// SMS: 'sms:<phone_number>?body=<message>'
// You can add a phone number, or keep it empty to allow the user to input one.
url = 'sms:?body=${Uri.encodeComponent(message)}';
break;
case InviteTypeEnum.email:
// Email: 'mailto:<email>?subject=<subject>&body=<message>'
// Email: 'mailto:<email>?subject=<subject>&body=<message>'
url = 'mailto:?subject=MowaterInvitation&body=${Uri.encodeComponent(message)}';
break;
}

@ -1368,24 +1368,14 @@ class AdVM extends BaseVM {
void onBackButtonPressed(BuildContext context) {
switch (currentProgressStep) {
case AdCreationStepsEnum.vehicleDetails:
// Check if user has entered any data
bool hasData = vehicleTypeId.selectedId != -1 ||
vehicleBrandId.selectedId != -1 ||
vehicleModelId.selectedId != -1 ||
vehicleModelYearId.selectedId != -1 ||
vehicleColorId.selectedId != -1 ||
vehicleCategoryId.selectedId != -1 ||
vehicleConditionId.selectedId != -1 ||
vehicleMileageId.selectedId != -1 ||
vehicleTransmissionId.selectedId != -1 ||
vehicleCountryId.selectedId != -1 ||
vehicleCityId.selectedId != -1 ||
vehicleDemandAmount.isNotEmpty ||
vehicleVin.isNotEmpty ||
vehicleTitle.isNotEmpty ||
warrantyDuration.isNotEmpty ||
vehicleDescription.isNotEmpty ||
pickedPostingImages.isNotEmpty;
// Only offer to save as a draft once both brand AND model are selected. Brand
// has no field of its own in the draft payload -- it's only ever recoverable by
// looking up model.vehicleBrandID when a draft is reopened. Confirmed on a real
// device: saving with a brand picked but no model meant the brand was silently
// lost on reopen, since there was nothing to derive it from. Any other field
// (description, images, etc.) being filled in isn't enough on its own -- a draft
// without a vehicle identified isn't meaningfully recoverable either.
bool hasData = vehicleBrandId.selectedId != -1 && vehicleModelId.selectedId != -1;
if (hasData) {
// Skip save confirmation when extending ad
@ -1601,7 +1591,6 @@ class AdVM extends BaseVM {
id: !isNew ? previousAdDetails!.vehiclePostingID : null,
userID: appState.getUser.data!.userInfo!.userId,
vehicleType: vehicleTypeId.selectedId,
vehicleBrandID: vehicleBrandId.selectedId,
vehicleModelID: vehicleModelId.selectedId,
vehicleModelYearID: vehicleModelYearId.selectedId,
vehicleColorID: vehicleColorId.selectedId,
@ -1690,7 +1679,6 @@ class AdVM extends BaseVM {
id: (previousAdDetails != null && previousAdDetails!.vehiclePostingID != null) ? previousAdDetails!.vehiclePostingID : null,
userID: appState.getUser.data!.userInfo!.userId,
vehicleType: vehicleTypeId.selectedId != -1 ? vehicleTypeId.selectedId : null,
vehicleBrandID: vehicleBrandId.selectedId != -1 ? vehicleBrandId.selectedId : null,
vehicleModelID: vehicleModelId.selectedId != -1 ? vehicleModelId.selectedId : null,
vehicleModelYearID: vehicleModelYearId.selectedId != -1 ? vehicleModelYearId.selectedId : null,
vehicleColorID: vehicleColorId.selectedId != -1 ? vehicleColorId.selectedId : null,
@ -2271,7 +2259,6 @@ class AdVM extends BaseVM {
id: (isAdEditEnabled || isExtendAdEditEnabled) && previousAdDetails != null && previousAdDetails!.vehiclePostingID != null ? previousAdDetails!.vehiclePostingID : null,
userID: appState.getUser.data!.userInfo!.userId,
vehicleType: vehicleTypeId.selectedId,
vehicleBrandID: vehicleBrandId.selectedId,
vehicleModelID: vehicleModelId.selectedId,
vehicleModelYearID: vehicleModelYearId.selectedId,
vehicleColorID: vehicleColorId.selectedId,
@ -3027,27 +3014,12 @@ class AdVM extends BaseVM {
if (vehicleBrands.isEmpty) {
await getVehicleBrandsByVehicleTypeId();
}
// Neither side of this is trustworthy alone: the top-level field survives even when no
// model is picked (the whole reason it was added), but as of now the backend still
// returns 0 for it even when a model/brand IS set (confirmed via a real draft response,
// model.vehicleBrandID was 2 but the top-level field was 0) -- needs flagging back to
// them. So treat null/0 on either side as "unusable" and take whichever one is actually
// a real id.
int topLevelBrandId = previousAdDetails!.vehicle!.vehicleBrandID ?? 0;
int modelBrandId = previousAdDetails!.vehicle!.model?.vehicleBrandID ?? 0;
int resolvedBrandId = topLevelBrandId > 0 ? topLevelBrandId : modelBrandId;
int index = vehicleBrands.indexWhere((element) => element.id == resolvedBrandId);
int index = vehicleBrands.indexWhere((element) => element.id == previousAdDetails!.vehicle!.model!.vehicleBrandID);
if (index != -1) {
await updateSelectionVehicleBrandId(SelectionModel(selectedId: vehicleBrands[index].id!, selectedOption: vehicleBrands[index].vehicleBrandDescription ?? ""));
}
// Same "0 means not set" convention as the other nested fields below -- an unset model
// comes back as a placeholder ({"id": 0, "label": null, ...}), not an absent field.
// Blindly trusting .id! here set selectedId to 0 instead of -1, which silently passed
// the `selectedId == -1` required-field check on Next even though no model was picked.
updateSelectionVehicleModelId((previousAdDetails!.vehicle!.model?.id ?? 0) > 0
? SelectionModel(selectedId: previousAdDetails!.vehicle!.model!.id!, selectedOption: previousAdDetails!.vehicle!.model!.label ?? "")
: SelectionModel(selectedOption: "", selectedId: -1, errorValue: ""));
updateSelectionVehicleModelId(SelectionModel(selectedId: previousAdDetails!.vehicle!.model!.id!, selectedOption: previousAdDetails!.vehicle!.model!.label ?? ""));
// Confirmed from a real draft API response: an unset nested field comes back as a
// non-null placeholder object ({"id": 0, "label": null, ...}), not an absent/null
// field -- same "0 means not set" convention the demandAmount/odometer/warantyYears

@ -365,11 +365,9 @@ class ServiceVM extends BaseVM {
}
resetCustomServiceForm() {
customServiceEnabledStatus = false;
customServiceTitle = "";
customServicePrice = "";
customerServiceTax = "15";
notifyListeners();
}
String customServiceTitleError = "";

@ -9,9 +9,7 @@ import 'package:http/src/response.dart';
import 'package:local_auth/local_auth.dart';
import 'package:mc_common_app/classes/app_state.dart';
import 'package:mc_common_app/classes/consts.dart';
import 'package:mc_common_app/config/dependency_injection.dart';
import 'package:mc_common_app/config/routes.dart';
import 'package:mc_common_app/services/firebase_service.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/main.dart';
@ -430,7 +428,7 @@ class UserVM extends BaseVM {
SharedPrefManager.setUserId(user.data!.userInfo!.userId ?? "");
SharedPrefManager.setRefreshToken(user.data!.refreshToken ?? "");
SharedPrefManager.setData(jsonEncode(user.data!.userInfo!.toJson()));
await _completeLoginNavigation(context);
navigateReplaceWithName(context, AppRoutes.dashboard);
} else {
Utils.showToast(LocaleKeys.onlyProviderApp.tr());
}
@ -441,7 +439,7 @@ class UserVM extends BaseVM {
SharedPrefManager.setUserId(user.data!.userInfo!.userId ?? "");
SharedPrefManager.setRefreshToken(user.data!.refreshToken ?? "");
SharedPrefManager.setData(jsonEncode(user.data!.userInfo!.toJson()));
await _completeLoginNavigation(context);
navigateReplaceWithName(context, AppRoutes.dashboard);
} else {
Utils.showToast(LocaleKeys.onlyCustomerApp.tr());
}
@ -476,7 +474,7 @@ class UserVM extends BaseVM {
SharedPrefManager.setUserId(user.data!.userInfo!.userId ?? "");
SharedPrefManager.setRefreshToken(user.data!.refreshToken ?? "");
SharedPrefManager.setData(jsonEncode(user.data!.userInfo!.toJson()));
await _completeLoginNavigation(context);
navigateReplaceWithName(context, AppRoutes.dashboard);
} else {
Utils.showToast(LocaleKeys.onlyProviderApp.tr());
//("Sorry, Only Customer's can log in this app");
@ -488,7 +486,7 @@ class UserVM extends BaseVM {
SharedPrefManager.setUserId(user.data!.userInfo!.userId ?? "");
SharedPrefManager.setRefreshToken(user.data!.refreshToken ?? "");
SharedPrefManager.setData(jsonEncode(user.data!.userInfo!.toJson()));
await _completeLoginNavigation(context);
navigateReplaceWithName(context, AppRoutes.dashboard);
} else {
Utils.showToast(LocaleKeys.onlyCustomerApp.tr());
}
@ -545,7 +543,7 @@ class UserVM extends BaseVM {
if (verifiedUser.messageStatus == 1) {
User user = User.fromJson(jsonDecode(response2.body));
AppState().setUser = user;
await _completeLoginNavigation(context);
navigateReplaceWithName(context, AppRoutes.dashboard);
} else {
Utils.showToast(verifiedUser.message ?? "");
}
@ -615,28 +613,14 @@ class UserVM extends BaseVM {
}
}
// getAllCountries() previously let a failed call (e.g. SocketException/ClientException
// from api_client.dart) propagate as an unhandled exception all the way to the UI --
// confirmed on a real device: no try/catch anywhere in this chain, so the user just saw
// a crash with no toast. Catch and surface it here instead, and fall back to an empty
// Country() so existing callers (which already null-check/iterate ".data") keep working.
Future<Country> getAllCountries() async {
try {
return await userRepo.getAllCountries();
} catch (e) {
Utils.handleException(e, null);
return Country();
}
return await userRepo.getAllCountries();
}
Future<void> getAllCountriesForUser() async {
userCountries = null;
userCities = null;
try {
userCountries = await userRepo.getAllCountries();
} catch (e) {
Utils.handleException(e, null);
}
userCountries = await userRepo.getAllCountries();
notifyListeners();
}
@ -770,15 +754,6 @@ class UserVM extends BaseVM {
// notifyListeners();
}
// Every successful login path (password, OTP, basic OTP) ends up here instead of
// navigating to the dashboard directly -- a notification tapped before login (cold
// start, expired session) stashes its data in AppState().pendingNotificationData, and
// this is the one place that resumes it so none of the 5 login call sites forget to.
Future<void> _completeLoginNavigation(BuildContext context) async {
navigateReplaceWithName(context, AppRoutes.dashboard);
await injector.get<FirebaseMessagingService>().resumePendingNotificationIfAny();
}
void logout(BuildContext context) async {
Response value = await userRepo.logoutUser();
if (value.body.isNotEmpty) {
@ -798,10 +773,6 @@ class UserVM extends BaseVM {
AppState().getUser.data!.userInfo!.userImageUrl = null;
}
AppState().setUser = null;
// Otherwise a notification tapped by the previous user (and still unresolved because
// they weren't logged in at the time) would fire for whoever logs in next on this
// device.
AppState().setPendingNotificationData = null;
if (AppState().currentAppType == AppType.provider) {
AppState().setproviderSubscription = null;
context.read<SubscriptionsVM>().mySubscriptionsBySp.clear();

@ -1110,27 +1110,11 @@ class BuildAdDetailsActionButtonForMyAds extends StatelessWidget {
title: LocaleKeys.no.tr(),
txtColor: MyColors.darkPrimaryColor,
fontSize: 15,
onPressed: () async {
onPressed: () {
adVM.updateSelectionVehicleTypeId(
SelectionModel(selectedId: adDetailsModel.vehicle!.vehicleType!, selectedOption: (adDetailsModel.vehicle!.vehicleType!).toVehicleTypeString(), errorValue: ""),
);
if (AppState().currentAppType == AppType.provider && AppState().hasAdsRemainingInActiveSubscription) {
// Subscription covers this extension -- skip the duration sheet and
// extend for free. adsDurationId: 0 is a placeholder since no duration
// is actually picked here; the backend decides isPaymentRequired from
// subscription status, not from this id.
List<bool> statuses = await adVM.createAdExtensionOrder(context, adId: adDetailsModel.id ?? -1, adsDurationId: 0);
if (statuses[0] && statuses[1]) {
navigateWithName(context, AppRoutes.paymentMethodsView, arguments: PaymentTypes.extendAds);
} else if (statuses[0] == true && statuses[1] == false) {
pop(context);
pop(context);
adVM.applyFilterOnMyAds(adPostStatusEnum: AdPostStatus.active);
}
return;
}
showMyBottomSheet(context, child: AdDurationSelectionSheet(isFromExtendAd: true, isUpdateAdSelected: false, adsID: adDetailsModel.id ?? -1));
},
),

@ -42,27 +42,6 @@ class AdDurationSelectionSheet extends StatelessWidget {
LocaleKeys.selectDuration.tr().toText(fontSize: 22, isBold: true).paddingOnly(top: 10, left: 21, right: 21),
],
),
// Shown only when a provider lands here because their subscription can't cover
// this ad/extension for free (expired, or no ads left in the plan) -- otherwise
// this sheet wouldn't be shown to them at all. Explains why they're being asked
// to pick a paid duration here instead of it being covered automatically.
if (AppState().currentAppType == AppType.provider && !AppState().hasAdsRemainingInActiveSubscription) ...[
8.height,
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Icon(Icons.info_outline_rounded, size: 18, color: MyColors.textColor),
8.width,
Expanded(
child: LocaleKeys.adChargeDueToSubscriptionNotice.tr().toText(fontSize: 13, color: MyColors.textColor),
),
],
).toContainer(
backgroundColor: MyColors.greyButtonColor,
paddingAll: 10,
borderRadius: 6,
).paddingOnly(left: 21, right: 21, top: 8),
],
Expanded(
child: ListView.separated(
shrinkWrap: true,
@ -199,7 +178,6 @@ class AdDurationSelectionSheet extends StatelessWidget {
maxWidth: double.infinity,
margin: const EdgeInsets.all(21),
onPressed: () async {
log("isFromExtendAd && !isUpdateAdSelected: ${isFromExtendAd && !isUpdateAdSelected}");
if (adVM.isFetchingLists) {
return;
} else {

@ -167,7 +167,7 @@ class _AdCardState extends State<AdCard> {
child: Column(
children: [
LocaleKeys.formatNeedsUpdate.tr().toText(),
// "Ad format needs to be updated.".toText(),
// "Ad format needs to be updated.".toText(),
],
),
),
@ -214,7 +214,7 @@ class _AdCardState extends State<AdCard> {
),
// Image indicator dots
if (hasMultipleImages)
Positioned(
Positioned(
bottom: 6,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
@ -282,7 +282,6 @@ class _AdCardState extends State<AdCard> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
flex: 2,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
@ -313,16 +312,12 @@ class _AdCardState extends State<AdCard> {
else if (isAdsFragment && widget.adDetails.adPostStatus! == AdPostStatus.sold && widget.adDetails.statuslabel != null && widget.adDetails.statuslabel!.isNotEmpty) ...[
Padding(
padding: const EdgeInsets.only(top: 5),
child: Utils.statusContainerChip(
text: isArabic ? widget.adDetails.statuslabelN ?? "" : widget.adDetails.statuslabel ?? "",
chipColor: Utils.getChipColorByAdStatus(widget.adDetails.adPostStatus!)),
child: Utils.statusContainerChip(text: isArabic ? widget.adDetails.statuslabelN ?? "": widget.adDetails.statuslabel ?? "", chipColor: Utils.getChipColorByAdStatus(widget.adDetails.adPostStatus!)),
),
] else if (isAdsFragment && !context.read<AdVM>().isExploreAdsTapped && widget.adDetails.statuslabel != null && widget.adDetails.statuslabel!.isNotEmpty) ...[
Padding(
padding: const EdgeInsets.only(top: 5),
child: Utils.statusContainerChip(
text: isArabic ? widget.adDetails.statuslabelN ?? "" : widget.adDetails.statuslabel ?? "",
chipColor: Utils.getChipColorByAdStatus(widget.adDetails.adPostStatus!)),
child: Utils.statusContainerChip(text: isArabic ? widget.adDetails.statuslabelN ?? "": widget.adDetails.statuslabel ?? "", chipColor: Utils.getChipColorByAdStatus(widget.adDetails.adPostStatus!)),
),
],
(widget.adDetails.vehicle!.vehicleTitle ?? "").toText(
@ -357,8 +352,7 @@ class _AdCardState extends State<AdCard> {
2.width,
Expanded(
child: "${widget.adDetails.vehicle?.mileage?.mileageStart ?? ""} - ${widget.adDetails.vehicle?.mileage?.mileageEnd ?? ""}"
"${LocaleKeys.km.tr()}"
.toText(
"${LocaleKeys.km.tr()}".toText(
overflow: TextOverflow.ellipsis,
fontSize: 12,
letterSpacing: -0.48,
@ -380,14 +374,8 @@ class _AdCardState extends State<AdCard> {
// : const SizedBox(),
// ],
children: [
(isArabic ? widget.adDetails.vehicle!.cityNameN ?? "" : widget.adDetails.vehicle!.cityName ?? "").toText(color: MyColors.lightTextColor, fontSize: 4),
widget.adDetails.createdOn != null
? DateTime.parse(widget.adDetails.createdOn!)
.getTimeAgo(
isArabic: context.locale.languageCode == 'ar',
)
.toText(color: MyColors.lightTextColor, fontSize: 4)
: const SizedBox(),
(isArabic ? widget.adDetails.vehicle!.cityNameN ?? "" : widget.adDetails.vehicle!.cityName ?? "").toText(color: MyColors.lightTextColor, fontSize: 4),
widget.adDetails.createdOn != null ? DateTime.parse(widget.adDetails.createdOn!).getTimeAgo(isArabic: context.locale.languageCode == 'ar',).toText(color: MyColors.lightTextColor, fontSize: 4) : const SizedBox(),
],
),
],
@ -402,9 +390,7 @@ class _AdCardState extends State<AdCard> {
children: [
(widget.adDetails.vehicle!.demandAmount!.toInt().toString()).toText(fontSize: 19, isBold: true, letterSpacing: -1.16, height: 29 / 19),
2.width,
(isArabic ? widget.adDetails.vehicle!.currencyN ?? "" : widget.adDetails.vehicle!.currency ?? "")
.toText(color: MyColors.lightTextColor, fontSize: 10, letterSpacing: -0.4, height: 16 / 10)
.paddingOnly(bottom: 3),
(isArabic ? widget.adDetails.vehicle!.currencyN ?? "" : widget.adDetails.vehicle!.currency ?? "").toText(color: MyColors.lightTextColor, fontSize: 10, letterSpacing: -0.4, height: 16 / 10).paddingOnly(bottom: 3),
],
),
),

@ -80,8 +80,13 @@ class BuildFooterButton extends StatelessWidget {
case AdCreationStepsEnum.vehicleDetails:
return Row(
children: [
// Hide "Save as Draft" when extending ad
if (!adVm.isExtendAdEditEnabled) ...[
// Hide "Save as Draft" when extending ad, and until brand+model are both
// selected -- brand has no field of its own in the draft payload, it's
// only ever recoverable via model.vehicleBrandID on reload, so saving with
// a brand picked but no model silently loses the brand (confirmed on a
// real device). Same gate as the back-button confirmation in
// onBackButtonPressed.
if (!adVm.isExtendAdEditEnabled && adVm.vehicleBrandId.selectedId != -1 && adVm.vehicleModelId.selectedId != -1) ...[
Expanded(
child: ShowFillButton(
txtColor: MyColors.black,

@ -77,14 +77,13 @@ class SelectAdTypeView extends StatelessWidget {
VehicleTypeModel vehicleTypeModel = adVM.vehicleTypes[index];
return InkWell(
onTap: () async {
adVM.updateSelectionVehicleTypeId(SelectionModel(
selectedId: vehicleTypeModel.id!, selectedOption: isArabic ? vehicleTypeModel.vehicleTypeNameN ?? "" : vehicleTypeModel.vehicleTypeName ?? "", errorValue: ""));
adVM.updateSelectionVehicleTypeId(SelectionModel(selectedId: vehicleTypeModel.id!, selectedOption: isArabic ? vehicleTypeModel.vehicleTypeNameN ?? "" : vehicleTypeModel.vehicleTypeName ?? "", errorValue: ""));
// Clear previous ad details when creating NEW ad (not editing)
if (adsId == -1 && !adVM.isAdEditEnabled && !adVM.isDraftEditEnabled) {
adVM.previousAdDetails = null;
}
if (adVM.isAdEditEnabled) {
adVM.autoFillSelectedVehicleAdsDetails(context);
}
@ -100,12 +99,8 @@ class SelectAdTypeView extends StatelessWidget {
pop(context);
adVM.applyFilterOnMyAds(adPostStatusEnum: AdPostStatus.active);
}
} else if (AppState().hasAdsRemainingInActiveSubscription) {
navigateReplaceWithName(context, AppRoutes.createAdView);
} else {
// Subscription expired or no ads left in it -- provider pays for this
// ad/extension just like a customer would, so collect a duration first.
showMyBottomSheet(context, child: AdDurationSelectionSheet(isUpdateAdSelected: adVM.isAdEditEnabled, isFromExtendAd: isFromExtendAd, adsID: adsId));
navigateReplaceWithName(context, AppRoutes.createAdView);
}
} else {
showMyBottomSheet(context, child: AdDurationSelectionSheet(isUpdateAdSelected: adVM.isAdEditEnabled, isFromExtendAd: isFromExtendAd, adsID: adsId));
@ -125,17 +120,13 @@ class SelectAdTypeView extends StatelessWidget {
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
isArabic ? vehicleTypeModel.vehicleTypeNameN.toString().toText(fontSize: 18) : vehicleTypeModel.vehicleTypeName.toString().toText(fontSize: 18),
// vehicleTypeModel.vehicleTypeName.toString().toText(fontSize: 18),
isArabic ? vehicleTypeModel.vehicleTypeNameN.toString().toText(fontSize: 18)
: vehicleTypeModel.vehicleTypeName.toString().toText(fontSize: 18),
// vehicleTypeModel.vehicleTypeName.toString().toText(fontSize: 18),
getVehicleAdTypeIcon(vehicleTypeModel.vehicleTypeEnum!),
],
),
// A provider only gets the "covered by subscription" display when
// their subscription actually covers it -- otherwise (expired, or no
// ads left) tapping this takes them to the paid duration sheet just
// like a customer, so the display here needs to say so too instead
// of claiming subscription coverage that no longer applies.
if (isProvider && AppState().hasAdsRemainingInActiveSubscription) ...[
if (isProvider) ...[
Row(
children: [
"${LocaleKeys.duration.tr()}: ".toText(fontSize: 12, color: MyColors.lightTextColor, isBold: true),
@ -148,13 +139,12 @@ class SelectAdTypeView extends StatelessWidget {
children: [
LocaleKeys.duration.tr().toText(fontSize: 12, color: MyColors.lightTextColor, isBold: true),
4.width,
(adVM.vehicleAdDurationId.selectedOption.isNotEmpty ? adVM.vehicleAdDurationId.selectedOption : LocaleKeys.selectDuration.tr())
.toText(fontSize: 13, isBold: true),
adVM.vehicleAdDurationId.selectedOption.toText(fontSize: 13, isBold: true),
const Icon(Icons.keyboard_arrow_down_sharp, color: MyColors.darkPrimaryColor, size: 20),
],
).paddingOnly(top: 5, bottom: 5),
],
if (!isProvider || !AppState().hasAdsRemainingInActiveSubscription) ...[
if (!isProvider) ...[
Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [

@ -55,22 +55,55 @@ class AppointmentServicePickBottomSheet extends StatelessWidget {
children: [
Builder(
builder: (context) {
print("isArabic ===== $isArabic");
for (var item in appointmentsVM.branchCategories) {
print("id ===== ${item.id}");
print("value English ===== ${item.value}");
print("valueN Arabic ===== ${item.valueN}");
print("subValue Price ===== ${item.subValue}");
print(
"Displayed ===== ${isArabic ? (item.valueN ?? item.value) : item.value}",
);
}
for (var item in appointmentsVM.branchCategories) {
print('id: ${item.id}');
print('value: ${item.value}');
print('subValue: ${item.subValue}');
print("valueN ${item.valueN}");
}
// return DropdownField(
// (DropValue value) => appointmentsVM.updateProviderCategoryId(SelectionModel(selectedId: value.id, selectedOption: value.value, itemPrice: value.subValue)),
// list: appointmentsVM.branchCategories,
// hint: LocaleKeys.selectCategory.tr(),
// dropdownValue: appointmentsVM.branchSelectedCategoryId.selectedId != -1
// ? DropValue(appointmentsVM.branchSelectedCategoryId.selectedId, appointmentsVM.branchSelectedCategoryId.selectedOption, "")
// : null,
// );
return DropdownField(
(DropValue value) => appointmentsVM.updateProviderCategoryId(
(DropValue value) => appointmentsVM.updateProviderCategoryId(
SelectionModel(
selectedId: value.id,
selectedOption: isArabic ? (value.subValue ?? value.value) : value.value,
selectedOption: isArabic
? (value.subValue ?? value.value)
: value.value,
),
),
list: appointmentsVM.branchCategories,
hint: LocaleKeys.selectCategory.tr(),
dropdownValue: appointmentsVM.branchSelectedCategoryId.selectedId != -1 &&
appointmentsVM.branchCategories.any(
(item) => item.id == appointmentsVM.branchSelectedCategoryId.selectedId,
)
dropdownValue:
appointmentsVM.branchSelectedCategoryId.selectedId != -1 &&
appointmentsVM.branchCategories.any(
(item) =>
item.id ==
appointmentsVM.branchSelectedCategoryId.selectedId,
)
? appointmentsVM.branchCategories.firstWhere(
(item) => item.id == appointmentsVM.branchSelectedCategoryId.selectedId,
)
(item) =>
item.id ==
appointmentsVM.branchSelectedCategoryId.selectedId,
)
: null,
isArabic: isArabic,
useSubValueForArabic: true,

@ -1,149 +0,0 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:mc_common_app/config/routes.dart';
import 'package:mc_common_app/theme/colors.dart';
/// Self-dismissing toast with a visible countdown, shown on top of whatever view is
/// currently on screen. Mounts via the root navigator's Overlay, so callers don't need
/// to pass a BuildContext (avoids the stale-context issues a dialog-based approach has,
/// e.g. when shown from a VM/repo layer that doesn't hold one).
class AppToast {
static OverlayEntry? _entry;
// Single place to retune how long an action-performing toast waits before firing its
// onComplete (either naturally, or immediately if the user taps it early -- see _dismiss).
static const int actionSeconds = 4;
// Duration for the plain toast with no onComplete callback -- change this single value
// to retune it everywhere that uses it.
static const int plainSeconds = 3;
static const Color errorBgColor = Color(0xFFF1CECE);
static const Color errorTextColor = MyColors.redColor;
static const Color successBgColor = Color(0xFFECFDF5);
static const Color successTextColor = MyColors.greenColor;
static void show(String message, {int seconds = 5, VoidCallback? onComplete, bool showIcon = true, bool showCountdownText = true, Color? bgColor, Color? textColor}) {
OverlayState? overlay = navigatorKey.currentState?.overlay;
if (overlay == null) return;
// Only one toast on screen at a time -- a new call replaces whatever is currently
// showing. Its onComplete (if any) never fires in that case -- _dismiss only acts on
// the entry that's still current.
_entry?.remove();
_entry = null;
late OverlayEntry entry;
entry = OverlayEntry(
builder: (BuildContext context) => _CountdownToast(
message: message,
seconds: seconds,
showIcon: showIcon,
showCountdownText: showCountdownText,
bgColor: bgColor,
textColor: textColor,
onDismiss: () => _dismiss(entry, onComplete),
),
);
_entry = entry;
overlay.insert(entry);
}
// Fires on both the timer completing and an early tap -- a tap just means "don't make
// me wait", not "cancel this". Guarded so a toast that's already been replaced never
// fires its (now stale) action.
static void _dismiss(OverlayEntry entry, VoidCallback? onComplete) {
if (_entry == entry) {
entry.remove();
_entry = null;
onComplete?.call();
}
}
}
class _CountdownToast extends StatefulWidget {
final String message;
final int seconds;
final VoidCallback onDismiss;
final bool showIcon;
final bool showCountdownText;
final Color? bgColor;
final Color? textColor;
const _CountdownToast({required this.message, required this.seconds, required this.onDismiss, this.showIcon = true, this.showCountdownText = true, this.bgColor, this.textColor});
@override
State<_CountdownToast> createState() => _CountdownToastState();
}
class _CountdownToastState extends State<_CountdownToast> {
late int _remaining = widget.seconds;
Timer? _timer;
@override
void initState() {
super.initState();
_timer = Timer.periodic(const Duration(seconds: 1), (Timer t) {
setState(() => _remaining--);
if (_remaining <= 0) {
t.cancel();
widget.onDismiss();
}
});
}
@override
void dispose() {
_timer?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
Color bgColor = widget.bgColor ?? MyColors.darkTextColor;
Color textColor = widget.textColor ?? Colors.white;
return Positioned(
left: 16,
right: 16,
top: 24,
child: SafeArea(
bottom: false,
child: Material(
color: Colors.transparent,
child: GestureDetector(
onTap: widget.onDismiss,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(10),
boxShadow: const <BoxShadow>[BoxShadow(color: Color(0x33000000), blurRadius: 12, offset: Offset(0, 4))],
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
if (widget.showIcon) ...<Widget>[Icon(Icons.error_outline, color: textColor, size: 20), const SizedBox(width: 10)],
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(widget.message, style: TextStyle(color: textColor, fontSize: 14, height: 1.3, fontWeight: FontWeight.w600)),
if (widget.showCountdownText) ...<Widget>[
const SizedBox(height: 4),
Text("Dismissing in ${_remaining}s", style: TextStyle(color: textColor.withOpacity(0.7), fontSize: 12, fontWeight: FontWeight.w500)),
],
],
),
),
],
),
),
),
),
),
);
}
}

@ -38,6 +38,10 @@ class FiltersList extends StatelessWidget {
itemCount: filterList.length,
scrollDirection: Axis.horizontal,
itemBuilder: (BuildContext context, int index) {
print("isArabic ===== $isArabic");
print("title EN ===== ${filterList[index].title}");
print("title AR ===== ${filterList[index].titleN}");
print("--------------------");
return InkWell(
onTap: () {
onFilterTapped(index, filterList[index].id);
@ -68,7 +72,8 @@ class FiltersList extends StatelessWidget {
],
],
// Text displayed in the chip
(isArabic ? (filterList[index].titleN.isNotEmpty ? filterList[index].titleN : filterList[index].title) : filterList[index].title).toText(
(isArabic
? (filterList[index].titleN.isNotEmpty ? filterList[index].titleN : filterList[index].title) : filterList[index].title).toText(
fontSize: 13,
letterSpacing: -0.14,
color: filterList[index].isSelected ? MyColors.white : null,

Loading…
Cancel
Save