diff --git a/lib/extensions/string_extensions.dart b/lib/extensions/string_extensions.dart index 00fc619..395d887 100644 --- a/lib/extensions/string_extensions.dart +++ b/lib/extensions/string_extensions.dart @@ -597,6 +597,8 @@ extension AdReserveStatusEnum on int { } else if (this == 5) { return AdReserveStatus.dealDone; } else if (this == 6) { + return AdReserveStatus.paymentDoneByCustomer; + } else if (this == 7) { return AdReserveStatus.fullPaymentVerified; } else { return AdReserveStatus.defaultStatus; diff --git a/lib/models/advertisment_models/ad_details_model.dart b/lib/models/advertisment_models/ad_details_model.dart index c89dbdc..ced36a7 100644 --- a/lib/models/advertisment_models/ad_details_model.dart +++ b/lib/models/advertisment_models/ad_details_model.dart @@ -43,6 +43,9 @@ class AdDetailsModel { String? modifiedOn; AdPostStatus? adPostStatus; AdReserveStatus? adReserveStatus; + int? adsReserveId; // Reserve ID from AdsReserve_Get API + bool? isReservable; + String? reserveCustomerMessage; bool? isMyAd; bool? isReservedByMe; bool? isAdFavorite; @@ -92,6 +95,9 @@ class AdDetailsModel { this.isOnWhatsApp, this.adPostStatus, this.adReserveStatus, + this.adsReserveId, + this.isReservable, + this.reserveCustomerMessage, this.isMyAd, this.isReservedByMe, this.isAdFavorite, @@ -160,6 +166,8 @@ class AdDetailsModel { adOwnerDetails = (json['vehicle'] != null && json['vehicle']['aDsUser'] != null) ? (AdOwnerDetails.fromJson(json['vehicle']['aDsUser'])) : null; adPostStatus = ((json['statusID'] ?? 1) as int).toAdPostEnum(); adReserveStatus = AdReserveStatus.defaultStatus; + isReservable = json['isReservable'] ?? false; + reserveCustomerMessage = json['reserveCustomerMessage']; createdByRoleEnum = (json['createdByRole'] as int).toCreatedByRoleEnum(); isMyAd = isMyAds; isReservedByMe = false; diff --git a/lib/repositories/ads_repo.dart b/lib/repositories/ads_repo.dart index 50448e5..680fbd7 100644 --- a/lib/repositories/ads_repo.dart +++ b/lib/repositories/ads_repo.dart @@ -74,7 +74,7 @@ abstract class AdsRepo { Future deleteAd({required int adId}); - Future cancelMyAdReservation({required int adId, required int adsReserveStatus, required String reason}); + Future cancelMyAdReservation({required int adId, required int adsReserveId, required int adsReserveStatus, required String reason}); Future createReserveAd({required int adId}); @@ -450,6 +450,7 @@ class AdsRepoImp implements AdsRepo { Future> getMyReservedAds() async { var params = { "userID": appState.getUser.data!.userInfo!.userId ?? "", + "IsAdCurrentlyReserved": "true", }; GenericRespModel adsGenericModel = await apiClient.getJsonForObject( @@ -492,26 +493,29 @@ class AdsRepoImp implements AdsRepo { List vehicleAdsDetails = List.generate(adsGenericModel.data.length, (index) => AdDetailsModel.fromJson(adsGenericModel.data[index], true, adsGenericModel.totalItemsCount ?? 1)); - // Create a map for O(1) lookup: adsID -> reserve status + // Create maps for O(1) lookup: adsID -> reserve status and reserve ID Map statusMap = {}; + Map reserveIdMap = {}; // adsID -> reserve ID for (var reserved in reservedAds) { log("adsReserveStatus: ${reserved.adsReserveStatus}"); statusMap[reserved.adsID ?? 0] = (reserved.adsReserveStatus ?? 0).toAdRserveStatusEnum(); + reserveIdMap[reserved.adsID ?? 0] = reserved.id ?? 0; // Store reserve ID } log("📋 Status Map created: ${statusMap.length} entries"); statusMap.forEach((key, value) { - log(" Ad ID: $key -> Status: $value"); + log(" Ad ID: $key -> Status: $value, Reserve ID: ${reserveIdMap[key]}"); }); - // Assign reserve status by matching ad ID (not by index!) + // Assign reserve status and reserve ID by matching ad ID (not by index!) log("📦 Processing ${vehicleAdsDetails.length} ads from second API"); for (var ad in vehicleAdsDetails) { if (statusMap.containsKey(ad.id)) { ad.adReserveStatus = statusMap[ad.id]!; + ad.adsReserveId = reserveIdMap[ad.id]!; // Store reserve ID // Backend will set adPostStatus=reserved in the response, so no need to set it manually - log(" ✅ Ad ${ad.id}: Assigned adReserveStatus ${ad.adReserveStatus}"); + log(" ✅ Ad ${ad.id}: Assigned adReserveStatus ${ad.adReserveStatus}, adsReserveId ${ad.adsReserveId}"); } else { // Ad exists but no reserve status found - shouldn't happen but handle it ad.adReserveStatus = AdReserveStatus.defaultStatus; @@ -689,10 +693,11 @@ class AdsRepoImp implements AdsRepo { } @override - Future cancelMyAdReservation({required int adId, required int adsReserveStatus, required String reason}) async { + Future cancelMyAdReservation({required int adId, required int adsReserveId, required int adsReserveStatus, required String reason}) async { int customerID = AppState().getUser.data!.userInfo!.customerId ?? 0; var postParams = { + "ID": adsReserveId, // Reserve ID (mandatory as per backend requirement) "adsID": adId, "adsReserveStatus": adsReserveStatus, "comment": reason, @@ -793,7 +798,7 @@ class AdsRepoImp implements AdsRepo { String token = appState.getUser.data!.accessToken ?? ""; GenericRespModel adsGenericModel = await apiClient.postJsonForObject( (json) => GenericRespModel.fromJson(json), - "", + ApiConsts.reserveAdPaymentOnDealDoneCreate, postParams, token: token, ); diff --git a/lib/utils/enums.dart b/lib/utils/enums.dart index f386e12..b248827 100644 --- a/lib/utils/enums.dart +++ b/lib/utils/enums.dart @@ -44,6 +44,7 @@ enum AdReserveStatus { cancelledByAdmin, timeOver, dealDone, + paymentDoneByCustomer, fullPaymentVerified, } diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart index 0ea0b16..de2a614 100644 --- a/lib/utils/utils.dart +++ b/lib/utils/utils.dart @@ -276,6 +276,11 @@ class Utils { return MyColors.adCancelledStatusColor; // Cancelled - red case AdReserveStatus.dealDone: + return MyColors.textColor; // pending - gray + + case AdReserveStatus.paymentDoneByCustomer: + return MyColors.textColor; // paymentDone - green + case AdReserveStatus.fullPaymentVerified: return MyColors.greenColor; // Completed successfully - green @@ -299,8 +304,11 @@ class Utils { case AdReserveStatus.cancelledByAdmin: return LocaleKeys.cancelledByAdmin.tr(); - case AdReserveStatus.dealDone: - return LocaleKeys.dealCompleted.tr(); + case AdReserveStatus.dealDone: // means pending complete payment + return LocaleKeys.pendingForPayment.tr(); + + case AdReserveStatus.paymentDoneByCustomer: + return LocaleKeys.waitingAdminsResponse.tr(); case AdReserveStatus.fullPaymentVerified: return LocaleKeys.paymentVerified.tr(); diff --git a/lib/view_models/ad_view_model.dart b/lib/view_models/ad_view_model.dart index 77e148f..8bed210 100644 --- a/lib/view_models/ad_view_model.dart +++ b/lib/view_models/ad_view_model.dart @@ -14,6 +14,7 @@ import 'package:mc_common_app/main.dart'; import 'package:mc_common_app/models/advertisment_models/ad_details_model.dart'; import 'package:mc_common_app/models/advertisment_models/ads_bank_details_model.dart'; import 'package:mc_common_app/models/advertisment_models/ads_duration_model.dart'; +import 'package:mc_common_app/models/advertisment_models/pdf_receipt_model.dart'; import 'package:mc_common_app/models/advertisment_models/special_service_model.dart'; import 'package:mc_common_app/models/advertisment_models/ss_car_check_schedule_model.dart'; import 'package:mc_common_app/models/advertisment_models/ss_photo_schedule_model.dart'; @@ -522,7 +523,7 @@ class AdVM extends BaseVM { Future getAdBankingAccountInfo({required int adId}) async { setState(ViewState.busy); try { - adsBankDetailsModel = await adsRepo.getAdBankingAccountInfo(adId: 21); + adsBankDetailsModel = await adsRepo.getAdBankingAccountInfo(adId: adId); setState(ViewState.idle); } catch (e) { setState(ViewState.idle); @@ -562,20 +563,20 @@ class AdVM extends BaseVM { navigateReplaceWithName(context, AppRoutes.dashboard, arguments: DashboardRouteEnum.fromAdsSubmit); } - Future cancelMyAdReservation(BuildContext context, {required int adId, required String reason}) async { + Future cancelMyAdReservation(BuildContext context, {required int adId, required int adsReserveId, required String reason}) async { Utils.showLoading(context); - GenericRespModel respModel = await adsRepo.cancelMyAdReservation(adId: adId, adsReserveStatus: 11, reason: reason); // 11 is to Cancel Reservation + GenericRespModel respModel = await adsRepo.cancelMyAdReservation(adId: adId, adsReserveId: adsReserveId, adsReserveStatus: 2, reason: reason); // 2 for AdsReserveStatus is to Cancel Reservation if (respModel.messageStatus != 1) { Utils.hideLoading(context); Utils.showToast(respModel.message ?? LocaleKeys.somethingWrong.tr()); return; } - + // Reset the cancel reason text field reservationCancelReason = ""; reservationCancelError = ""; - + Utils.hideLoading(context); Utils.showToast(LocaleKeys.yourReservationCancelled.tr()); updateIsExploreAds(false); @@ -1773,31 +1774,81 @@ class AdVM extends BaseVM { notifyListeners(); } - List pickedReceiptPdfFiles = []; + // Single receipt file for Complete Deal flow + ImageModel? pickedReceiptFile; - String receiptPdfFileError = ""; + String receiptFileError = ""; - void removePdfFileFromList(String filePath) { - int index = pickedReceiptPdfFiles.indexWhere((element) => element.filePath == filePath); - if (index == -1) { - return; - } - pickedReceiptPdfFiles.removeAt(index); + void removeReceiptFile() { + pickedReceiptFile = null; notifyListeners(); } - void pickPdfReceiptFile(BuildContext context) async { - List imageModels = []; - List? files = await commonServices.pickMultipleFiles(context); - if (files == null) return; - for (var element in files) { - imageModels.add(ImageModel(filePath: element.path, isFromNetwork: false)); - } - pickedReceiptPdfFiles.addAll(imageModels); - if (pickedReceiptPdfFiles.isNotEmpty) receiptPdfFileError = ""; + void pickReceiptFile(BuildContext context) async { + List? files = await commonServices.pickMultipleFiles(context, allowMultiple: false); + if (files == null || files.isEmpty) return; + + pickedReceiptFile = ImageModel(filePath: files.first.path, isFromNetwork: false); + if (pickedReceiptFile != null) receiptFileError = ""; notifyListeners(); } + // Upload bank receipt for Complete Deal + Future uploadBankReceipt(BuildContext context, {required int adId}) async { + if (pickedReceiptFile == null) { + receiptFileError = LocaleKeys.selectAttachment.tr(); + notifyListeners(); + return; + } + + try { + Utils.showLoading(context); + + // Convert file to base64 + File file = File(pickedReceiptFile!.filePath!); + List fileBytes = await file.readAsBytes(); + String base64File = base64Encode(fileBytes); + String fileName = file.path.split('/').last; + + // Create PdfReceiptModel for API + PdfReceiptModel pdfReceipt = PdfReceiptModel( + id: 0, + pdfName: fileName, + pdfString: base64File, + ); + + // Call repository method + GenericRespModel response = await adsRepo.uploadBankReceiptsOnReserveDealDone( + adId: adId, + detailNote: completeDealNotesForAdmin, + pdfReceiptsList: [pdfReceipt], + ); + + Utils.hideLoading(context); + + if (response.messageStatus == 1) { + Utils.showToast(response.message ?? "Receipt uploaded successfully"); + + // Reset values + pickedReceiptFile = null; + completeDealNotesForAdmin = ""; + receiptFileError = ""; + notifyListeners(); + + // Close bottom sheet + if (context.mounted) { + Navigator.of(context).pop(); + } + } else { + Utils.showToast(response.message ?? "Failed to upload receipt"); + } + } catch (e) { + Utils.hideLoading(context); + Utils.showToast("Error uploading receipt: $e"); + log("Error in uploadBankReceipt: $e"); + } + } + // sourceFlag for Camera = 0 // sourceFlag for Gallery = 1 void pickDamagePartImage(int index) async { @@ -1882,6 +1933,9 @@ class AdVM extends BaseVM { vehicleDamageCards.clear(); specialServiceCards.clear(); vehicleAdsSpecialServices.clear(); + pickedReceiptFile = null; + completeDealNotesForAdmin = ""; + receiptFileError = ""; currentProgressStep = AdCreationStepsEnum.vehicleDetails; vehicleTypeId = SelectionModel(selectedOption: "", selectedId: -1, errorValue: ""); vehicleModelId = SelectionModel(selectedOption: "", selectedId: -1, errorValue: ""); diff --git a/lib/view_models/dashboard_view_model_customer.dart b/lib/view_models/dashboard_view_model_customer.dart index f465923..fa8492b 100644 --- a/lib/view_models/dashboard_view_model_customer.dart +++ b/lib/view_models/dashboard_view_model_customer.dart @@ -119,10 +119,20 @@ class DashboardVmCustomer extends BaseVM with WidgetsBindingObserver { await appointmentsVM.applyFilterOnBranches(index: 0); // to get all branches! await appointmentsVM.getMyRecentBranches(); // to get my recent branches - if (dashboardRouteEnum != DashboardRouteEnum.fromAdsPayment && dashboardRouteEnum != DashboardRouteEnum.fromAdsSubmit) { + if (dashboardRouteEnum != DashboardRouteEnum.fromAdsPayment && + dashboardRouteEnum != DashboardRouteEnum.fromAdsSubmit && + dashboardRouteEnum != DashboardRouteEnum.fromReserveAdsPayment) { await adVM.getMyAds(); await adVM.getExploreAds(); } + + // Handle reserve ads payment completion + if (dashboardRouteEnum == DashboardRouteEnum.fromReserveAdsPayment) { + adVM.updateIsExploreAds(false); + onNavbarTapped(2); // Go to My Ads tab (customer has different tab index) + await adVM.applyFilterOnMyAds(adPostStatusEnum: AdPostStatus.reserved); + log("✅ Successfully navigated to Reserved Ads after payment"); + } await requestsVM.getRequests(); await adVM.getVehicleTypes(); diff --git a/lib/view_models/dashboard_view_model_provider.dart b/lib/view_models/dashboard_view_model_provider.dart index 9da5397..c7c5261 100644 --- a/lib/view_models/dashboard_view_model_provider.dart +++ b/lib/view_models/dashboard_view_model_provider.dart @@ -1,3 +1,4 @@ +import 'dart:developer'; import 'dart:io'; import 'package:flutter/widgets.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; @@ -51,11 +52,11 @@ class DashboardVMProvider extends BaseVM with WidgetsBindingObserver { void didChangeAppLifecycleState(AppLifecycleState state) { super.didChangeAppLifecycleState(state); logger.i("App lifecycle state changed to: $state"); - + if (_context == null) return; - + final chatVM = Provider.of(_context!, listen: false); - + switch (state) { case AppLifecycleState.resumed: // App came back to foreground - ensure SignalR is connected @@ -122,10 +123,10 @@ class DashboardVMProvider extends BaseVM with WidgetsBindingObserver { final serviceVM = context.read(); final adVM = context.read(); final subscriptionsVM = context.read(); - + // ✅ Inject ViewModels into ChatVM (only needs to be done once, but safe to repeat) chatVM.setViewModels(requestsVM: requestsVM, adVM: adVM); - + await requestsVM.populateDataForRequestsFilter(); await requestsVM.populateDeliveryOptionEnums(); await appointmentVM.populateAppointmentsFilterList(); @@ -135,10 +136,19 @@ class DashboardVMProvider extends BaseVM with WidgetsBindingObserver { await requestsVM.getRequests(); await subscriptionsVM.getSubscriptionBySP(AppState().getUser.data?.userInfo?.providerId.toString() ?? "", true); - if (dashboardRouteEnum != DashboardRouteEnum.fromAdsPayment && dashboardRouteEnum != DashboardRouteEnum.fromAdsSubmit) { + if (dashboardRouteEnum != DashboardRouteEnum.fromAdsPayment && dashboardRouteEnum != DashboardRouteEnum.fromAdsSubmit && dashboardRouteEnum != DashboardRouteEnum.fromReserveAdsPayment) { await adVM.getMyAds(); await adVM.getExploreAds(); } + + // Handle reserve ads payment completion + if (dashboardRouteEnum == DashboardRouteEnum.fromReserveAdsPayment) { + adVM.updateIsExploreAds(false); + onNavbarTapped(3); // Go to My Ads tab + await adVM.applyFilterOnMyAds(adPostStatusEnum: AdPostStatus.reserved); + log("✅ Successfully navigated to Reserved Ads after payment"); + } + await shippingManagementVM.populateShippingRequestFilterList(); await shippingManagementVM.populateSelfPickupRequestFilterList(); await appointmentVM.applyFilterOnAppointmentsVMForProviders( diff --git a/lib/view_models/payment_view_model.dart b/lib/view_models/payment_view_model.dart index 44c8d54..7ebcde2 100644 --- a/lib/view_models/payment_view_model.dart +++ b/lib/view_models/payment_view_model.dart @@ -246,39 +246,16 @@ class PaymentVM extends ChangeNotifier { void onAdReservePaymentSuccess(BuildContext context) { log("onAdReservePaymentSuccess - Reserve ads payment completed"); - // Always use navigatorKey for reliable navigation after payment - // Payment flow often unmounts context, so navigatorKey is more reliable + // Navigate to dashboard with reserved ads route argument + // The dashboard's onRefresh method will handle setting up the reserved filter log("Navigating to dashboard with reserved ads filter"); - - // Navigate to dashboard first navigatorKey.currentState?.pushNamedAndRemoveUntil( AppRoutes.dashboard, (route) => false, arguments: DashboardRouteEnum.fromReserveAdsPayment, ); - // Use a post-frame callback to ensure navigation completes before updating state - WidgetsBinding.instance.addPostFrameCallback((_) { - final context = navigatorKey.currentContext; - if (context != null && context.mounted) { - try { - // Navigate to My Ads tab and apply Reserved filter - context.read().updateIsExploreAds(false); - if (AppState().currentAppType == AppType.provider) { - context.read().onNavbarTapped(3); // My Ads tab - context.read().applyFilterOnMyAds(adPostStatusEnum: AdPostStatus.reserved); - } else { - context.read().onNavbarTapped(3); // My Ads tab - context.read().applyFilterOnMyAds(adPostStatusEnum: AdPostStatus.reserved); - } - log("✅ Successfully navigated to Reserved Ads"); - } catch (e) { - log("⚠️ Error updating AdVM after navigation: $e"); - } - } else { - log("⚠️ Context not available for post-navigation updates"); - } - }); + log("✅ Navigation initiated - Dashboard will handle filter setup"); } void onSubscriptionPaymentSuccess() { diff --git a/lib/views/advertisement/ads_detail_view/ads_detail_view.dart b/lib/views/advertisement/ads_detail_view/ads_detail_view.dart index f8b4506..2e6c277 100644 --- a/lib/views/advertisement/ads_detail_view/ads_detail_view.dart +++ b/lib/views/advertisement/ads_detail_view/ads_detail_view.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:developer'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:mc_common_app/classes/app_state.dart'; @@ -50,9 +51,10 @@ class _AdsDetailViewState extends State { } Future onAdDetailsLoaded() async { + log("${widget.adDetails.isMyAd}, ${widget.adDetails.isReservedByMe} ,${widget.adDetails.adPostStatus}"); context.read().updateCurrentAdId(id: widget.adDetails.id!); // Load banking info when viewing MY created ad that someone else reserved - if ((widget.adDetails.isMyAd ?? false) && !(widget.adDetails.isReservedByMe ?? false) && (widget.adDetails.adPostStatus == AdPostStatus.reserved)) { + if ((widget.adDetails.isMyAd ?? false) && (widget.adDetails.isReservedByMe ?? false) && (widget.adDetails.adPostStatus == AdPostStatus.reserved)) { await adVM.getAdBankingAccountInfo(adId: widget.adDetails.id!); } } diff --git a/lib/views/advertisement/ads_detail_view/components.dart b/lib/views/advertisement/ads_detail_view/components.dart index 1d8cdf0..d609433 100644 --- a/lib/views/advertisement/ads_detail_view/components.dart +++ b/lib/views/advertisement/ads_detail_view/components.dart @@ -47,6 +47,25 @@ class BuildAdDetailsActionButtonForExploreAds extends StatelessWidget { description: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + // Show instructions if available + if (adDetailsModel.reserveCustomerMessage != null && adDetailsModel.reserveCustomerMessage!.isNotEmpty) ...[ + LocaleKeys.shippingManagementInstruction.tr().toText(fontSize: 16, isBold: true), + 8.height, + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: MyColors.lightGreyBgColor, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: MyColors.borderColor), + ), + child: adDetailsModel.reserveCustomerMessage!.toText( + fontSize: 14, + maxLines: 10, + height: 1.5, + ), + ), + 20.height, + ], Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -91,7 +110,7 @@ class BuildAdDetailsActionButtonForExploreAds extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - LocaleKeys.carInsuranceService.tr().toText(fontSize: 14, isBold: true, color: MyColors.lightTextColor, fontWeight: FontWeight.w500), + LocaleKeys.carInsuranceService.tr().toText(fontSize: 14, color: MyColors.lightTextColor, fontWeight: FontWeight.w500), LocaleKeys.toBeDecided.tr().toText(fontSize: 12, isBold: true), ], ), @@ -99,7 +118,7 @@ class BuildAdDetailsActionButtonForExploreAds extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - LocaleKeys.registrationCarPlates.tr().toText(fontSize: 14, isBold: true, color: MyColors.lightTextColor), + LocaleKeys.registrationCarPlates.tr().toText(fontSize: 14, color: MyColors.lightTextColor), LocaleKeys.toBeDecided.tr().toText(fontSize: 12, isBold: true), ], ), @@ -107,7 +126,7 @@ class BuildAdDetailsActionButtonForExploreAds extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - LocaleKeys.homeDeliveryService.tr().toText(fontSize: 14, isBold: true, color: MyColors.lightTextColor), + LocaleKeys.homeDeliveryService.tr().toText(fontSize: 14, color: MyColors.lightTextColor), LocaleKeys.toBeDecided.tr().toText(fontSize: 12, isBold: true), ], ), @@ -262,7 +281,13 @@ class BuildAdDetailsActionButtonForExploreAds extends StatelessWidget { case CreatedByRoleEnum.provider: return defaultActionForProviderAndCustomer(context, adDetailsModel); case CreatedByRoleEnum.admin: - return reserveAdAction(context, adDetailsModel); + // Check if admin ad is reservable + if (adDetailsModel.isReservable == true && adDetailsModel.adPostStatus != AdPostStatus.sold) { + return reserveAdAction(context, adDetailsModel); + } else { + // Show contact buttons just like customer/provider ads + return defaultActionForProviderAndCustomer(context, adDetailsModel); + } case CreatedByRoleEnum.allAds: return const SizedBox.shrink(); } @@ -760,7 +785,7 @@ class BuildAdDetailsActionButtonForMyAds extends StatelessWidget { Future buildCancelReservationBottomSheet(BuildContext context, {required AdDetailsModel adDetails}) { // Capture parent context that will remain valid final BuildContext parentContext = context; - + return showModalBottomSheet( context: context, isScrollControlled: true, @@ -799,18 +824,22 @@ class BuildAdDetailsActionButtonForMyAds extends StatelessWidget { // Save reason before showing confirmation dialog final String cancelReason = adVM.reservationCancelReason; return actionConfirmationBottomSheet( - context: parentContext, // Use parent context + context: parentContext, + // Use parent context title: LocaleKeys.doWantCancelReservation.tr().toText(fontSize: 28, isBold: true, letterSpacing: -1.44), - subtitle: "yourAdVisibletoBuy.".tr(), + subtitle: LocaleKeys.yourAdVisibletoBuy.tr(), actionButtonYes: Expanded( child: ShowFillButton( maxHeight: 55, title: LocaleKeys.yes.tr(), fontSize: 15, onPressed: () async { - Navigator.pop(parentContext); // Pop confirmation - // Call with parent context that's still valid - await adVM.cancelMyAdReservation(parentContext, adId: adDetails.id!, reason: cancelReason); + Navigator.pop(parentContext); // Pop confirmation + // Call with parent context that's still valid, pass reserve ID + await adVM.cancelMyAdReservation(parentContext, + adId: adDetails.id!, + adsReserveId: adDetails.adsReserveId ?? 0, // Pass reserve ID + reason: cancelReason); }, ), ), @@ -910,42 +939,93 @@ class BuildAdDetailsActionButtonForMyAds extends StatelessWidget { ), 15.height, LocaleKeys.attachFile.tr().toText(fontSize: 20, isBold: true, letterSpacing: -0.5), - if (adVM.pickedReceiptPdfFiles.isNotEmpty) ...[ - 16.height, - PickedFilesContainer( - pickedFiles: adVM.pickedReceiptPdfFiles, - onCrossPressedPrimary: adVM.removePdfFileFromList, - onAddFilePressed: () { - context.read().pickPdfReceiptFile(context); - }, - isPdf: true, - ), + 16.height, + if (adVM.pickedReceiptFile != null) ...[ + // Show selected file + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: MyColors.lightGreyBgColor, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: MyColors.borderColor), + ), + child: Row( + children: [ + Icon( + adVM.pickedReceiptFile!.filePath!.endsWith('.pdf') ? Icons.picture_as_pdf : Icons.image, + size: 40, + color: MyColors.primaryColor, + ), + 12.width, + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + adVM.pickedReceiptFile!.filePath!.split('/').last.toText(fontSize: 14, isBold: true, maxLines: 1), + 4.height, + LocaleKeys.change.tr().toText( + fontSize: 12, + color: MyColors.lightTextColor, + ), + ], + ), + ), + IconButton( + icon: const Icon(Icons.close, color: MyColors.redColor), + onPressed: () { + context.read().removeReceiptFile(); + }, + ), + ], + ), + ).onPress(() { + context.read().pickReceiptFile(context); + }), ] else ...[ + // Show add file button (original design) Row( children: [ Container( height: 90, width: 90, - decoration: BoxDecoration(color: MyColors.greyButtonColor, border: Border.all(width: 2, color: MyColors.greyAddBorderColor)), + decoration: BoxDecoration( + color: MyColors.greyButtonColor, + border: Border.all(width: 2, color: MyColors.greyAddBorderColor), + ), margin: const EdgeInsets.all(8), alignment: Alignment.center, child: Container( height: 24, width: 24, - decoration: const BoxDecoration(shape: BoxShape.circle, color: MyColors.darkTextColor), + decoration: const BoxDecoration( + shape: BoxShape.circle, + color: MyColors.darkTextColor, + ), child: const Icon(Icons.add, color: MyColors.white), ), ).onPress(() { - context.read().pickPdfReceiptFile(context); + context.read().pickReceiptFile(context); }), ], ), ], + if (adVM.receiptFileError.isNotEmpty) ...[ + 8.height, + adVM.receiptFileError.toText( + fontSize: 12, + color: MyColors.redColor, + ), + ], 15.height, ShowFillButton( title: LocaleKeys.submit.tr(), onPressed: () { - //Upload Attachment + if (adVM.pickedReceiptFile == null) { + adVM.receiptFileError = LocaleKeys.selectAttachment.tr(); + adVM.notifyListeners(); + return; + } + adVM.uploadBankReceipt(context, adId: adDetails.id!); }, maxWidth: double.infinity, ), @@ -1035,6 +1115,9 @@ class BuildAdDetailsActionButtonForMyAds extends StatelessWidget { } return pendingForReviewAction(pendingText: LocaleKeys.waitingAdminsResponse.tr()); + case AdReserveStatus.paymentDoneByCustomer: + return pendingForReviewAction(pendingText: LocaleKeys.waitingAdminsResponse.tr()); + case AdReserveStatus.fullPaymentVerified: return pendingForReviewAction(pendingText: LocaleKeys.paymentVerified.tr()); @@ -1045,8 +1128,6 @@ class BuildAdDetailsActionButtonForMyAds extends StatelessWidget { @override Widget build(BuildContext context) { - log("adDetailsModel.adPostStatus: ${adDetailsModel.adPostStatus}"); - switch (adDetailsModel.adPostStatus!) { case AdPostStatus.pendingForPayment: return pendingForPaymentAction(context, ad: adDetailsModel); diff --git a/lib/views/advertisement/components/ads_list_widget.dart b/lib/views/advertisement/components/ads_list_widget.dart index 475f205..d724cfb 100644 --- a/lib/views/advertisement/components/ads_list_widget.dart +++ b/lib/views/advertisement/components/ads_list_widget.dart @@ -235,7 +235,6 @@ class _AdCardState extends State { if (widget.adDetails.adPostStatus == AdPostStatus.sold) ...[ // Blur overlay ClipRRect( - borderRadius: BorderRadius.circular(8), child: BackdropFilter( filter: ImageFilter.blur(sigmaX: 3, sigmaY: 3), child: Container( @@ -248,10 +247,7 @@ class _AdCardState extends State { // SOLD text Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - decoration: BoxDecoration( - color: Colors.red, - borderRadius: BorderRadius.circular(4), - ), + decoration: const BoxDecoration(color: Colors.red), child: LocaleKeys.sold.tr().toText( fontSize: 12, color: Colors.white, @@ -275,25 +271,38 @@ class _AdCardState extends State { crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start, children: [ + // Status tags row (for explore ads) + if (isAdsFragment && context.read().isExploreAdsTapped) ...[ + Wrap( + spacing: 4, + runSpacing: 4, + children: [ + // Show "Mowater Ad" tag for admin ads + if (widget.adDetails.createdByRoleEnum == CreatedByRoleEnum.admin) Utils.statusContainerChip(text: "Mowater Ad", chipColor: MyColors.primaryColor), + // Show Accident Free chip when isAccidentFree is true + if (widget.adDetails.vehicle?.isAccidentFree == true) Utils.statusContainerChip(text: LocaleKeys.accidentFree.tr(), chipColor: Colors.green), + ], + ), + ], // When viewing reserved ads filter, show ONLY reserve status badge - if (widget.adDetails.isReservedByMe == true && - widget.adDetails.adReserveStatus != null && + if (widget.adDetails.isReservedByMe == true && + widget.adDetails.adReserveStatus != null && widget.adDetails.adReserveStatus != AdReserveStatus.defaultStatus && context.read().isReservedFilterSelected) ...[ Utils.statusContainerChip( - text: Utils.getReserveStatusLabel(widget.adDetails.adReserveStatus!), - chipColor: Utils.getChipColorByReserveStatus(widget.adDetails.adReserveStatus!) - ), + text: Utils.getReserveStatusLabel(widget.adDetails.adReserveStatus!), chipColor: Utils.getChipColorByReserveStatus(widget.adDetails.adReserveStatus!)), ] // Otherwise, show regular ad status for MyAds else if (isAdsFragment && widget.adDetails.adPostStatus! == AdPostStatus.sold && widget.adDetails.statuslabel != null && widget.adDetails.statuslabel!.isNotEmpty) ...[ - Utils.statusContainerChip(text: widget.adDetails.statuslabel ?? "", chipColor: Utils.getChipColorByAdStatus(widget.adDetails.adPostStatus!)), + Padding( + padding: const EdgeInsets.only(top: 5), + child: Utils.statusContainerChip(text: widget.adDetails.statuslabel ?? "", chipColor: Utils.getChipColorByAdStatus(widget.adDetails.adPostStatus!)), + ), ] else if (isAdsFragment && !context.read().isExploreAdsTapped && widget.adDetails.statuslabel != null && widget.adDetails.statuslabel!.isNotEmpty) ...[ - Utils.statusContainerChip(text: widget.adDetails.statuslabel ?? "", chipColor: Utils.getChipColorByAdStatus(widget.adDetails.adPostStatus!)), - ], - // Show Accident Free chip for explore ads when isAccidentFree is true - if (isAdsFragment && context.read().isExploreAdsTapped && widget.adDetails.vehicle?.isAccidentFree == true) ...[ - Utils.statusContainerChip(text: LocaleKeys.accidentFree.tr(), chipColor: Colors.green), + Padding( + padding: const EdgeInsets.only(top: 5), + child: Utils.statusContainerChip(text: widget.adDetails.statuslabel ?? "", chipColor: Utils.getChipColorByAdStatus(widget.adDetails.adPostStatus!)), + ), ], (widget.adDetails.vehicle!.vehicleTitle ?? "").toText( fontSize: 16,