import 'package:flutter/material.dart'; import 'package:mc_common_app/classes/consts.dart'; import 'package:mc_common_app/config/routes.dart'; import 'package:mc_common_app/extensions/int_extensions.dart'; import 'package:mc_common_app/extensions/string_extensions.dart'; import 'package:mc_common_app/models/appointments_models/appointment_list_model.dart'; import 'package:mc_common_app/models/enums_model.dart'; import 'package:mc_common_app/models/generic_resp_model.dart'; import 'package:mc_common_app/models/provider_branches_models/branch_detail_model.dart'; import 'package:mc_common_app/models/provider_branches_models/provider_profile_model.dart'; import 'package:mc_common_app/models/service_schedule_model.dart'; import 'package:mc_common_app/models/services/item_model.dart'; import 'package:mc_common_app/models/services/service_model.dart'; import 'package:mc_common_app/models/widgets_models.dart'; import 'package:mc_common_app/repositories/appointment_repo.dart'; import 'package:mc_common_app/repositories/common_repo.dart'; import 'package:mc_common_app/repositories/provider_repo.dart'; import 'package:mc_common_app/services/common_services.dart'; import 'package:mc_common_app/theme/colors.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/base_view_model.dart'; import 'package:mc_common_app/view_models/dashboard_view_model_customer.dart'; import 'package:mc_common_app/view_models/payment_view_model.dart'; import 'package:mc_common_app/views/appointments/book_appointment_schedules_view.dart'; import 'package:mc_common_app/views/appointments/widgets/appointment_service_pick_bottom_sheet.dart'; import 'package:mc_common_app/widgets/common_widgets/info_bottom_sheet.dart'; import 'package:mc_common_app/widgets/dropdown/dropdow_field.dart'; import 'package:mc_common_app/widgets/extensions/extensions_widget.dart'; import 'package:provider/provider.dart'; class AppointmentsVM extends BaseVM { final CommonRepo commonRepo; final CommonAppServices commonServices; final ProviderRepo providerRepo; final AppointmentRepo scheduleRepo; AppointmentsVM({required this.commonServices, required this.scheduleRepo, required this.providerRepo, required this.commonRepo}); bool isFetchingLists = false; List myAppointments = []; List myUpComingAppointments = []; List myFilteredAppointments = []; List appointmentsFilterOptions = []; // List availableSchedules = []; bool isFetchingServices = false; List branchCategories = []; bool isHomeTapped = false; List serviceAppointmentScheduleList = []; bool ifItemAlreadySelected(int id) { int indexFound = allSelectedItemsInAppointments.indexWhere((element) => element.id == id); if (indexFound != -1) { return true; } return false; } List allSelectedItemsInAppointments = []; Future onItemsSelectedInService() async { if (currentServiceSelection != null) { int index = servicesInCurrentAppointment.indexWhere((element) => element.serviceId == currentServiceSelection!.serviceId!); if (index == -1) { double totalPrice = 0.0; currentServiceSelection!.serviceItems!.forEach((element) { totalPrice = totalPrice + double.parse(element.price ?? "0.0"); }); currentServiceSelection!.currentTotalServicePrice = totalPrice; servicesInCurrentAppointment.insert(0, currentServiceSelection!); } resetCategorySelectionBottomSheet(); notifyListeners(); } } Future onBookAppointmentPressed(BuildContext context) async { Utils.showLoading(context); bool isSuccess = false; List appointmentIdsList = []; try { GenericRespModel genericRespModel = await scheduleRepo.createServiceAppointment( schedules: serviceAppointmentScheduleList, serviceProviderID: selectedBranchModel!.serviceProviderId ?? 0, ); if (genericRespModel.messageStatus == 2 || genericRespModel.data == null) { Utils.hideLoading(context); Utils.showToast("${genericRespModel.message.toString()}"); return; } if (genericRespModel.data != null) { genericRespModel.data.forEach((element) { if (element['appointmentID'] != 0) { appointmentIdsList.add(element['appointmentID']); isSuccess = true; } else { isSuccess = false; return; } }); } context.read().onNavbarTapped(1); applyFilterOnAppointmentsVM(appointmentStatusEnum: AppointmentStatusEnum.booked); Utils.hideLoading(context); resetAfterBookingAppointment(); if (isSuccess) { if (amountToPayForAppointment > 0) { context.read().updateAppointmentIdsForPayment(ids: appointmentIdsList); navigateWithName(context, AppRoutes.paymentMethodsView, arguments: PaymentTypes.appointment); } else { Utils.showToast("Your appointment has been booked successfully!"); getMyAppointments(); navigateReplaceWithNameUntilRoute(context, AppRoutes.dashboard); } } } catch (e) { Utils.showToast("${e.toString()}"); } } Future onConfirmAppointmentPressed({required BuildContext context, required appointmentId}) async { context.read().updateAppointmentIdsForPayment(ids: [appointmentId]); navigateWithName(context, AppRoutes.paymentMethodsView, arguments: PaymentTypes.appointment); } Future onCancelAppointmentPressed({required BuildContext context, required AppointmentListModel appointmentListModel}) async { Utils.showLoading(context); try { GenericRespModel genericRespModel = await scheduleRepo.cancelOrRescheduleServiceAppointment( serviceAppointmentID: appointmentListModel.id ?? 0, serviceSlotID: appointmentListModel.serviceSlotID ?? 0, appointmentScheduleAction: 2, // 1 for Reschedule and 2 for Cancel ); if (genericRespModel.messageStatus == 2 || genericRespModel.data == null) { Utils.hideLoading(context); Utils.showToast("${genericRespModel.message.toString()}"); return; } if (genericRespModel.data == 1) { context.read().onNavbarTapped(1); applyFilterOnAppointmentsVM(appointmentStatusEnum: AppointmentStatusEnum.cancelled); Utils.showToast("${genericRespModel.message.toString()}"); await getMyAppointments(); Utils.hideLoading(context); getMyAppointments(); navigateReplaceWithNameUntilRoute(context, AppRoutes.dashboard); } } catch (e) { Utils.showToast("${e.toString()}"); } } void updateIsHomeTapped(bool value) { isHomeTapped = value; if (currentServiceSelection != null) { currentServiceSelection!.isHomeSelected = value; } notifyListeners(); } String pickedHomeLocation = ""; void updatePickedHomeLocation(String value) { pickedHomeLocation = value; pickHomeLocationError = ""; if (currentServiceSelection != null) { currentServiceSelection!.homeLocation = value; } notifyListeners(); } SelectionModel branchSelectedCategoryId = SelectionModel(selectedOption: "", selectedId: -1, errorValue: ""); void updateProviderCategoryId(SelectionModel id) { branchSelectedCategoryId = id; getBranchServices(categoryId: branchSelectedCategoryId.selectedId); notifyListeners(); } List providersFilterOptions = []; List nearbyBranches = []; BranchDetailModel? selectedBranchModel; List branchServices = []; List servicesInCurrentAppointment = []; ServiceModel? currentServiceSelection; void updateBranchServiceId(SelectionModel id) async { branchSelectedServiceId = id; currentServiceSelection = branchServices.firstWhere((element) => element.serviceProviderServiceId == id.selectedId); notifyListeners(); } void removeServiceInCurrentAppointment(int index) { int serviceId = servicesInCurrentAppointment.elementAt(index).serviceProviderServiceId ?? -1; allSelectedItemsInAppointments.removeWhere((element) => element.serviceProviderServiceId == serviceId); servicesInCurrentAppointment.removeAt(index); notifyListeners(); } resetCategorySelectionBottomSheet() { selectedSubServicesCounter = 0; branchSelectedCategoryId = SelectionModel(selectedOption: "", selectedId: -1, errorValue: ""); isHomeTapped = false; branchSelectedServiceId = SelectionModel(selectedOption: "", selectedId: -1, errorValue: ""); currentServiceSelection = null; } resetAfterBookingAppointment() { allSelectedItemsInAppointments.clear(); servicesInCurrentAppointment.clear(); serviceAppointmentScheduleList.clear(); } List myAppointmentsEnum = []; populateAppointmentsFilterList() async { appointmentsFilterOptions.clear(); myAppointmentsEnum = await commonRepo.getEnumTypeValues(enumTypeID: 13); //TODO: 13 is to get Appointments Filter Enums for (int i = 0; i < myAppointmentsEnum.length; i++) { appointmentsFilterOptions.add(FilterListModel(title: myAppointmentsEnum[i].enumValueStr, isSelected: false, id: myAppointmentsEnum[i].enumValue)); } appointmentsFilterOptions.insert(0, FilterListModel(title: "All Appointments", isSelected: true, id: 0)); notifyListeners(); } applyFilterOnAppointmentsVM({required AppointmentStatusEnum appointmentStatusEnum}) { if (appointmentsFilterOptions.isEmpty) return; for (var value in appointmentsFilterOptions) { value.isSelected = false; } appointmentsFilterOptions[appointmentStatusEnum.getIdFromAppointmentStatusEnum()].isSelected = true; if (appointmentStatusEnum.getIdFromAppointmentStatusEnum() == 0) { myFilteredAppointments = myAppointments; notifyListeners(); return; } myFilteredAppointments = myAppointments.where((element) => element.appointmentStatusID! == appointmentStatusEnum.getIdFromAppointmentStatusEnum()).toList(); notifyListeners(); } Future getMyAppointments({bool isNeedToRebuild = false}) async { if (isNeedToRebuild) setState(ViewState.busy); myAppointments = await commonRepo.getMyAppointments(); myFilteredAppointments = myAppointments; myUpComingAppointments = myAppointments.where((element) => element.appointmentStatusEnum == AppointmentStatusEnum.booked).toList(); setState(ViewState.idle); // applyFilterOnAppointmentsVM(appointmentStatusEnum: AppointmentStatusEnum.allAppointments); notifyListeners(); } updateSelectedBranch(BranchDetailModel branchDetailModel) { selectedBranchModel = branchDetailModel; getBranchCategories(); notifyListeners(); } updateSelectedAppointmentDate({required int dateIndex, required int scheduleIndex}) { serviceAppointmentScheduleList[scheduleIndex].customTimeDateSlotList!.forEach((element) { element.date!.isSelected = false; }); serviceAppointmentScheduleList[scheduleIndex].customTimeDateSlotList![dateIndex].date!.isSelected = true; serviceAppointmentScheduleList[scheduleIndex].selectedDateIndex = dateIndex; final date = TimeSlotModel( date: serviceAppointmentScheduleList[scheduleIndex].customTimeDateSlotList![dateIndex].date!.date, slotId: serviceAppointmentScheduleList[scheduleIndex].customTimeDateSlotList![dateIndex].date!.slotId, isSelected: true, slot: "", ); serviceAppointmentScheduleList[scheduleIndex].selectedCustomTimeDateSlotModel = CustomTimeDateSlotModel(date: date); notifyListeners(); } updateSelectedAppointmentSlotByDate({required int scheduleIndex, required int slotIndex}) { serviceAppointmentScheduleList[scheduleIndex].customTimeDateSlotList!.forEach((element) { element.availableSlots!.forEach((element) => element.isSelected = false); }); int index = serviceAppointmentScheduleList[scheduleIndex].selectedDateIndex!; serviceAppointmentScheduleList[scheduleIndex].customTimeDateSlotList![index].availableSlots![slotIndex].isSelected = true; serviceAppointmentScheduleList[scheduleIndex].selectedCustomTimeDateSlotModel!.availableSlots = serviceAppointmentScheduleList[scheduleIndex].customTimeDateSlotList![index].availableSlots!; notifyListeners(); } double amountToPayForAppointment = 0.0; double totalAmount = 0.0; List serviceItemsFromApi = []; ProviderProfileModel? providerProfileModel; int selectedSubServicesCounter = 0; onItemUpdateOrSelected(int index, bool selected, int itemId) { int serviceIndex = servicesInCurrentAppointment.indexWhere((element) => element.serviceId == currentServiceSelection!.serviceId!); if (serviceIndex == -1) { return; } serviceItemsFromApi[index].isUpdateOrSelected = selected; serviceItemsFromApi[index].isHomeSelected = isHomeTapped; if (selected) { selectedSubServicesCounter = selectedSubServicesCounter + 1; selectSubServicesError = ""; currentServiceSelection!.serviceItems!.add(serviceItemsFromApi[index]); allSelectedItemsInAppointments.add(serviceItemsFromApi[index]); allSelectedItemsInAppointments.forEach((element) { if (!ifItemAlreadySelected(element.id!)) { servicesInCurrentAppointment[serviceIndex].serviceItems!.add(serviceItemsFromApi[index]); servicesInCurrentAppointment[serviceIndex].currentTotalServicePrice = servicesInCurrentAppointment[serviceIndex].currentTotalServicePrice + double.parse((serviceItemsFromApi[index].price) ?? "0.0"); } }); } if (!selected) { selectedSubServicesCounter = selectedSubServicesCounter - 1; currentServiceSelection!.serviceItems!.removeWhere((element) => element.id == itemId); allSelectedItemsInAppointments.removeWhere((element) => element.id == itemId); servicesInCurrentAppointment[serviceIndex].currentTotalServicePrice = servicesInCurrentAppointment[serviceIndex].currentTotalServicePrice - double.parse((serviceItemsFromApi[index].price) ?? "0.0"); servicesInCurrentAppointment[serviceIndex].serviceItems!.removeWhere((element) => element.id == itemId); } notifyListeners(); } populateBranchesFilterList() { providersFilterOptions.clear(); providersFilterOptions = [ FilterListModel(title: "All Providers", isSelected: true, id: 0), FilterListModel(title: "Maintenance", isSelected: false, id: 1), FilterListModel(title: "Oil Service", isSelected: false, id: 2), FilterListModel(title: "Accessories", isSelected: false, id: 3), FilterListModel(title: "Tire Service", isSelected: false, id: 4), FilterListModel(title: "Dent and Paint", isSelected: false, id: 5), ]; notifyListeners(); } applyFilterOnProviders({required int index}) { if (providersFilterOptions.isEmpty) return; for (var value in providersFilterOptions) { value.isSelected = false; } providersFilterOptions[index].isSelected = true; notifyListeners(); } getAllNearBranches({bool isNeedToRebuild = false}) async { //TODO: needs to lat,long into API nearbyBranches.clear(); if (isNeedToRebuild) setState(ViewState.busy); nearbyBranches = await providerRepo.getAllNearBranchAndServices(); setState(ViewState.idle); } Future> getServiceItems(int serviceId) async { serviceItemsFromApi.clear(); serviceItemsFromApi = await providerRepo.getServiceItems(serviceId); serviceItemsFromApi.forEach((item) { if (ifItemAlreadySelected(item.id!)) { item.isUpdateOrSelected = true; } }); setState(ViewState.idle); return serviceItemsFromApi; } getBranchAndServices(int providerId) async { providerProfileModel = null; providerProfileModel = await providerRepo.getBranchAndServices(providerId); setState(ViewState.idle); } String pickHomeLocationError = ""; String selectSubServicesError = ""; SelectionModel branchSelectedServiceId = SelectionModel(selectedOption: "", selectedId: -1, errorValue: ""); bool isCategoryAlreadyPresent(int id) { final contain = branchCategories.where((element) => element.id == id); if (contain.isEmpty) { return false; } return true; } void getBranchCategories() async { for (var value in selectedBranchModel!.branchServices!) { if (!isCategoryAlreadyPresent(value.categoryId!)) { branchCategories.add(DropValue(value.categoryId!, value.categoryName!, "")); } } notifyListeners(); } getBranchServices({required int categoryId}) async { branchSelectedServiceId = SelectionModel(selectedOption: "", selectedId: -1, errorValue: ""); isHomeTapped = false; pickedHomeLocation = ""; pickHomeLocationError = ""; if (categoryId != -1) { isFetchingServices = true; branchServices = getFilteredBranchServices(categoryId: categoryId); isFetchingServices = false; notifyListeners(); } } List getFilteredBranchServices({required int categoryId}) { List filteredServices = selectedBranchModel!.branchServices!.where((element) => element.categoryId == categoryId).toList(); return filteredServices; } void updatePickHomeLocationError(String value) { pickHomeLocationError = value; // notifyListeners(); } bool isServiceSelectionValidated() { if (branchSelectedServiceId.selectedId == -1) { return false; } if (isHomeTapped) { if (pickedHomeLocation == "") { updatePickHomeLocationError(GlobalConsts.homeLocationEmptyError); return false; } } return true; } bool validateItemsSelection() { for (var value in serviceItemsFromApi) { if (value.isUpdateOrSelected!) { return true; } } selectSubServicesError = "Please select at least one sub service"; notifyListeners(); return false; } String getTotalPrice(List serviceItems) { var totalPrice = 0.0; serviceItems.forEach((element) { totalPrice = totalPrice + (element.currentTotalServicePrice); }); return totalPrice.toString(); } void openTheAddServiceBottomSheet(BuildContext context, AppointmentsVM appointmentsVM) { showModalBottomSheet( context: context, isScrollControlled: true, enableDrag: true, builder: (BuildContext context) { return AppointmentServicePickBottomSheet(); }, ); } void priceBreakDownClicked(BuildContext context, ServiceModel selectedService) { showModalBottomSheet( context: context, isScrollControlled: true, enableDrag: true, builder: (BuildContext context) { double totalKms = 15.3; return InfoBottomSheet( title: "Charges Breakdown".toText(fontSize: 24, isBold: true), description: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ "Services".toText(fontSize: 16, isBold: true), Column( children: List.generate( selectedService.serviceItems!.length, (index) => Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ "${selectedService.serviceItems![index].name}".toText(fontSize: 12, color: MyColors.lightTextColor, isBold: true), "${selectedService.serviceItems![index].price} SAR".toText(fontSize: 12, isBold: true), ], ), ), ), Row( mainAxisAlignment: MainAxisAlignment.end, children: [ "${selectedService.currentTotalServicePrice} SAR".toText(fontSize: 16, isBold: true), ], ), if (selectedService.isHomeSelected) ...[ 20.height, "Home Location".toText(fontSize: 16, isBold: true), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ "${totalKms}km ".toText(fontSize: 12, color: MyColors.lightTextColor, isBold: true), "${selectedService.rangePricePerKm} x $totalKms".toText(fontSize: 12, isBold: true), ], ), 8.height, Row( mainAxisAlignment: MainAxisAlignment.end, children: [ "${selectedService.rangePricePerKm ?? 0 * totalKms} SAR".toText(fontSize: 16, isBold: true), ], ), ], 30.height, Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ "Total Amount ".toText(fontSize: 16, isBold: true), Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ (selectedService.isHomeSelected ? "${(selectedService.currentTotalServicePrice) + (double.parse((selectedService.rangePricePerKm ?? "0.0")) * totalKms)}" : "${selectedService.currentTotalServicePrice}") .toText(fontSize: 29, isBold: true), 2.width, "SAR".toText(color: MyColors.lightTextColor, fontSize: 16, isBold: true).paddingOnly(bottom: 5), ], ) ], ), 30.height, ], )); }); } void onReviewButtonPressed(BuildContext context) { bool isValidated = false; for (int i = 0; i < serviceAppointmentScheduleList.length; i++) { final schedule = serviceAppointmentScheduleList[i]; if (schedule.selectedCustomTimeDateSlotModel == null) { isValidated = false; break; } if (schedule.selectedCustomTimeDateSlotModel!.date == null || !schedule.selectedCustomTimeDateSlotModel!.date!.isSelected) { isValidated = false; break; } else { if (schedule.selectedCustomTimeDateSlotModel!.availableSlots == null) { isValidated = true; break; } else { TimeSlotModel slot = schedule.selectedCustomTimeDateSlotModel!.availableSlots!.firstWhere((element) => element.isSelected); if (slot.date.isNotEmpty) { isValidated = true; break; } } } } if (!isValidated) { Utils.showToast("You must select appointment time for each schedule's appointment."); return; } navigateWithName(context, AppRoutes.reviewAppointmentView); } void onServicesNextPressed(BuildContext context) async { Utils.showLoading(context); List serviceItemIdsForHome = []; List serviceItemIdsForWorkshop = []; allSelectedItemsInAppointments.forEach((serviceItem) { if (serviceItem.isHomeSelected!) { serviceItemIdsForHome.add(serviceItem.id!.toString()); } else { serviceItemIdsForWorkshop.add(serviceItem.id!.toString()); } }); serviceAppointmentScheduleList = await scheduleRepo.mergeServiceIntoAvailableSchedules( serviceItemIdsForHome: serviceItemIdsForHome, serviceItemIdsForWorkshop: serviceItemIdsForWorkshop, ); if (serviceAppointmentScheduleList.isEmpty) { Utils.hideLoading(context); Utils.showToast("There are no available appointments for selected Items."); return; } totalAmount = 0.0; amountToPayForAppointment = 0.0; serviceAppointmentScheduleList.forEach( (schedule) { amountToPayForAppointment = amountToPayForAppointment + (schedule.amountToPay ?? 0.0); totalAmount = totalAmount + (schedule.amountTotal ?? 0.0); }, ); Utils.hideLoading(context); navigateWithName(context, AppRoutes.bookAppointmenSchedulesView, arguments: ScreenArgumentsForAppointmentDetailPage(routeFlag: 1, appointmentId: 0)); // 1 For Creating an Appointment notifyListeners(); } Future onRescheduleAppointmentPressed({required BuildContext context, required AppointmentListModel appointmentListModel}) async { Utils.showLoading(context); List serviceItemIdsForHome = []; List serviceItemIdsForWorkshop = []; appointmentListModel.appointmentServicesList!.forEach((service) { service.serviceItems!.forEach((serviceItem) { serviceItemIdsForWorkshop.add(serviceItem.id!.toString()); // if (serviceItem.isHomeSelected ?? false) { // serviceItemIdsForHome.add(serviceItem.id!.toString()); // } else { // serviceItemIdsForWorkshop.add(serviceItem.id!.toString()); // } }); }); serviceAppointmentScheduleList = await scheduleRepo.mergeServiceIntoAvailableSchedules( serviceItemIdsForHome: serviceItemIdsForHome, serviceItemIdsForWorkshop: serviceItemIdsForWorkshop, ); if (serviceAppointmentScheduleList.isEmpty) { Utils.hideLoading(context); Utils.showToast("There are no available appointments for selected Items."); return; } Utils.hideLoading(context); navigateWithName( context, AppRoutes.bookAppointmenSchedulesView, arguments: ScreenArgumentsForAppointmentDetailPage(routeFlag: 2, appointmentId: appointmentListModel.id ?? 0), ); // 2 For Rescheduling an Appointment notifyListeners(); } Future onRescheduleAppointmentConfirmPressed({required BuildContext context, required int appointmentId, required int selectedSlotId}) async { Utils.showLoading(context); try { GenericRespModel genericRespModel = await scheduleRepo.cancelOrRescheduleServiceAppointment( serviceAppointmentID: appointmentId, serviceSlotID: selectedSlotId, appointmentScheduleAction: 1, // 1 for Reschedule and 2 for Cancel ); if (genericRespModel.messageStatus == 2 || genericRespModel.data == null) { Utils.hideLoading(context); Utils.showToast("${genericRespModel.message.toString()}"); return; } if (genericRespModel.data == 1) { context.read().onNavbarTapped(1); applyFilterOnAppointmentsVM(appointmentStatusEnum: AppointmentStatusEnum.cancelled); Utils.showToast("${genericRespModel.message.toString()}"); getMyAppointments(); Utils.hideLoading(context); navigateReplaceWithNameUntilRoute(context, AppRoutes.dashboard); } } catch (e) { Utils.showToast("${e.toString()}"); } } }