From 5bc5ca89546cb299a97797601efeebc869e92cc4 Mon Sep 17 00:00:00 2001 From: Faiz Hashmi Date: Wed, 11 Oct 2023 11:37:28 +0300 Subject: [PATCH 1/5] Started changing the Appointment Structure --- lib/config/customer_routes.dart | 4 +- lib/repositories/schedule_repo.dart | 35 ++++- lib/view_models/appointments_view_model.dart | 145 ++++++++++++++--- .../appointments/appointment_detail_view.dart | 2 +- .../book_appointment_schedules_view.dart | 9 ++ .../book_appointment_services_view.dart | 58 +------ .../book_appointments_item_view.dart | 29 ++-- .../appointments/review_appointment_view.dart | 146 +++++++++--------- ...appointment_service_pick_bottom_sheet.dart | 3 +- lib/views/provider/branch_detail_page.dart | 9 +- .../provider/sheet/items_list_sheet.dart | 6 +- pubspec.yaml | 2 +- 12 files changed, 271 insertions(+), 177 deletions(-) create mode 100644 lib/views/appointments/book_appointment_schedules_view.dart diff --git a/lib/config/customer_routes.dart b/lib/config/customer_routes.dart index 6cca179..e2823e5 100644 --- a/lib/config/customer_routes.dart +++ b/lib/config/customer_routes.dart @@ -18,7 +18,7 @@ import 'package:mc_common_app/views/advertisement/select_ad_type_view.dart'; import 'package:mc_common_app/views/payments/payment_methods_view.dart'; import 'package:mc_common_app/views/advertisement/ads_search_filter_view.dart'; - class CustomerAppRoutes { +class CustomerAppRoutes { static final Map routes = { AppRoutes.dashboard: (context) => DashboardPage(), AppRoutes.bookProviderAppView: (context) => BookProviderAppView(), @@ -30,7 +30,7 @@ import 'package:mc_common_app/views/advertisement/ads_search_filter_view.dart'; AppRoutes.bookAppointmenServicesView: (context) => BookAppointmentServicesView(), AppRoutes.bookAppointmentsItemView: (context) => BookAppointmentsItemView(), AppRoutes.reviewAppointmentView: (context) => ReviewAppointment(), - AppRoutes.paymentMethodsView: (context) => PaymentMethodsView(paymentType: ModalRoute.of(context)!.settings.arguments as PaymentTypesEnum), + AppRoutes.paymentMethodsView: (context) => PaymentMethodsView(paymentType: ModalRoute.of(context)!.settings.arguments as PaymentTypes), AppRoutes.branchDetailPage: (context) => BranchDetailPage(branchDetailModel: ModalRoute.of(context)!.settings.arguments as BranchDetailModel), AppRoutes.providerProfilePage: (context) => ProviderProfilePage(providerId: ModalRoute.of(context)!.settings.arguments as int), }; diff --git a/lib/repositories/schedule_repo.dart b/lib/repositories/schedule_repo.dart index 7938095..8fbe9e3 100644 --- a/lib/repositories/schedule_repo.dart +++ b/lib/repositories/schedule_repo.dart @@ -21,7 +21,9 @@ abstract class ScheduleRepo { Future updateServicesInSchedule(Map map); - Future> mergeServiceIntoAvailableSchedules({required List serviceItemIds}); + Future> mergeServiceIntoAvailableSchedules({required int appointmentType, required List serviceItemIds}); + + Future createServiceAppointment({required List serviceItemIds, required int serviceSlotID}); } class ScheduleRepoImp implements ScheduleRepo { @@ -71,18 +73,39 @@ class ScheduleRepoImp implements ScheduleRepo { return await injector.get().postJsonForObject((json) => MResponse.fromJson(json), ApiConsts.updateGroup, map, token: t); } - Future> mergeServiceIntoAvailableSchedules({required List serviceItemIds}) async { + Future> mergeServiceIntoAvailableSchedules({required int appointmentType, required List serviceItemIds}) async { String t = AppState().getUser.data!.accessToken ?? ""; - var dummyServiceIds = ["3", "2"]; - var queryParameters = {"ServiceItemIDs": dummyServiceIds}; - GenericRespModel adsGenericModel = await injector.get().getJsonForObject( + var queryParameters = { + "appointmentType": appointmentType, + "ServiceItemIDs": serviceItemIds, + }; + GenericRespModel adsGenericModel = await injector.get().postJsonForObject( (json) => GenericRespModel.fromJson(json), ApiConsts.GetServiceItemAppointmentScheduleSlots, + queryParameters, token: t, - queryParameters: queryParameters, ); List serviceAppointmentScheduleModel = List.generate(adsGenericModel.data.length, (index) => ServiceAppointmentScheduleModel.fromJson(adsGenericModel.data[index])); return serviceAppointmentScheduleModel; } + + Future createServiceAppointment({required List serviceItemIds, required int serviceSlotID}) async { + String t = AppState().getUser.data!.accessToken ?? ""; + var queryParameters = { + "id": 0, + "serviceSlotID": serviceSlotID, + "appointmentStatusID": 0, + "serviceProviderID": 0, + "customerID": 0, + "isActive": true, + "serviceItemID": [11] + }; + GenericRespModel adsGenericModel = await injector.get().postJsonForObject( + (json) => GenericRespModel.fromJson(json), + ApiConsts.ServiceProvidersAppointmentCreate, + queryParameters, + token: t, + ); + } } diff --git a/lib/view_models/appointments_view_model.dart b/lib/view_models/appointments_view_model.dart index 08889a9..f3b3deb 100644 --- a/lib/view_models/appointments_view_model.dart +++ b/lib/view_models/appointments_view_model.dart @@ -1,6 +1,8 @@ import 'package:car_customer_app/repositories/provider_repo.dart'; import 'package:car_customer_app/repositories/schedule_repo.dart'; +import 'package:flutter/cupertino.dart'; import 'package:mc_common_app/classes/consts.dart'; +import 'package:mc_common_app/config/routes.dart'; import 'package:mc_common_app/models/appointments_models/appointment_list_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'; @@ -11,6 +13,8 @@ import 'package:mc_common_app/models/widgets_models.dart'; import 'package:mc_common_app/repositories/common_repo.dart'; import 'package:mc_common_app/services/common_services.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/widgets/dropdown/dropdow_field.dart'; @@ -47,24 +51,61 @@ class AppointmentsVM extends BaseVM { Future mergeServiceIntoAvailableSchedules() async { List serviceItemIds = []; - serviceItems.forEach((serviceItem) { + currentServiceSelection!.serviceItems!.forEach((serviceItem) { if (serviceItem.isUpdateOrSelected!) { serviceItemIds.add(serviceItem.id!.toString()); } }); - var scheduleList = await scheduleRepo.mergeServiceIntoAvailableSchedules(serviceItemIds: serviceItemIds); - serviceAppointmentScheduleList.addAll(scheduleList); + + servicesInCurrentAppointment.add(currentServiceSelection!); + + serviceAppointmentScheduleList = await scheduleRepo.mergeServiceIntoAvailableSchedules( + serviceItemIds: serviceItemIds, + appointmentType: isHomeTapped ? 2 : 1, // AppointmentType 1 for workshop and 2 for Home based schedule. + ); + servicesInSchedule.clear(); - serviceAppointmentScheduleList.forEach((schedule) { - schedule.serviceItemList!.forEach((item) { - if (!ifItemAlreadyThere(item.serviceProviderServiceId!)) { - servicesInSchedule.add(DropValue(item.serviceProviderServiceId!, item.description!, "")); - } - }); - }); + + if (serviceAppointmentScheduleList.isEmpty) { + Utils.showToast("There are no available appointments for selected Items."); + return; + } + + serviceAppointmentScheduleList.forEach( + (schedule) { + amountToPayForAppointment = amountToPayForAppointment + (schedule.amountToPay ?? 0.0); + + schedule.serviceItemList!.forEach((item) { + if (!ifItemAlreadyThere(item.serviceProviderServiceId!)) { + servicesInSchedule.add(DropValue(item.serviceProviderServiceId!, item.description!, "")); + } + }); + }, + ); notifyListeners(); } + Future onItemsSelectedInService() async { + if (currentServiceSelection != null) { + servicesInCurrentAppointment.add(currentServiceSelection!); + } + } + + Future onBookAppointmentPressed(BuildContext context) async { + Utils.showLoading(context); + try { + serviceAppointmentScheduleList.forEach((schedule) async { + List serviceItemIds = []; + schedule.serviceItemList!.forEach((serviceItem) { + serviceItemIds.add(serviceItem.id.toString()); + }); + await scheduleRepo.createServiceAppointment(serviceItemIds: serviceItemIds, serviceSlotID: schedule.selectedCustomTimeDateSlotModel!.date!.slotId); + }); + } catch (e) { + Utils.showToast("${e.toString()}"); + } + } + bool ifItemAlreadyThere(int id) { int index = servicesInSchedule.indexWhere((element) => element.id == id); if (index == -1) { @@ -99,6 +140,8 @@ class AppointmentsVM extends BaseVM { BranchDetailModel? selectedBranchModel; List branchServices = []; + + List servicesInCurrentAppointment = []; ServiceModel? currentServiceSelection; void updateBranchServiceId(SelectionModel id) async { @@ -150,8 +193,70 @@ class AppointmentsVM extends BaseVM { notifyListeners(); } - List serviceItems = []; - List selectedServiceItems = []; + 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!; + print("here: ${serviceAppointmentScheduleList[scheduleIndex].customTimeDateSlotList![index].availableSlots![slotIndex].slotId}"); + notifyListeners(); + } + + double amountToPayForAppointment = 0.0; + double totalAmount = 0.0; + + 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); + } + + List serviceItemsFromApi = []; ProviderProfileModel? providerProfileModel; int selectedSubServicesCounter = 0; @@ -162,12 +267,12 @@ class AppointmentsVM extends BaseVM { } onItemUpdateOrSelected(int index, bool selected, int itemId) { - serviceItems[index].isUpdateOrSelected = selected; + serviceItemsFromApi[index].isUpdateOrSelected = selected; if (selected) { selectedSubServicesCounter = selectedSubServicesCounter + 1; updateSelectedSubServicesCounter(selectedSubServicesCounter); selectSubServicesError = ""; - currentServiceSelection!.serviceItems!.add(serviceItems[index]); + currentServiceSelection!.serviceItems!.add(serviceItemsFromApi[index]); } if (!selected) { selectedSubServicesCounter = selectedSubServicesCounter - 1; @@ -208,10 +313,10 @@ class AppointmentsVM extends BaseVM { } Future> getServiceItems(int serviceId) async { - serviceItems.clear(); - serviceItems = await providerRepo.getServiceItems(serviceId); + serviceItemsFromApi.clear(); + serviceItemsFromApi = await providerRepo.getServiceItems(serviceId); setState(ViewState.idle); - return serviceItems; + return serviceItemsFromApi; } getBranchAndServices(int providerId) async { @@ -263,7 +368,7 @@ class AppointmentsVM extends BaseVM { void updatePickHomeLocationError(String value) { pickHomeLocationError = value; - notifyListeners(); + // notifyListeners(); } bool isServiceSelectionValidated() { @@ -280,8 +385,8 @@ class AppointmentsVM extends BaseVM { return true; } - bool onSubServicesNextPressed() { - for (var value in serviceItems) { + bool validateItemsSelection() { + for (var value in serviceItemsFromApi) { if (value.isUpdateOrSelected!) { return true; } diff --git a/lib/views/appointments/appointment_detail_view.dart b/lib/views/appointments/appointment_detail_view.dart index 86e2dec..718d69d 100644 --- a/lib/views/appointments/appointment_detail_view.dart +++ b/lib/views/appointments/appointment_detail_view.dart @@ -10,7 +10,7 @@ import 'package:mc_common_app/widgets/common_widgets/card_button_with_icon.dart' import 'package:mc_common_app/widgets/extensions/extensions_widget.dart'; class AppointmentDetailView extends StatelessWidget { - AppointmentListModel appointmentListModel; + final AppointmentListModel appointmentListModel; AppointmentDetailView({Key? key, required this.appointmentListModel}) : super(key: key); diff --git a/lib/views/appointments/book_appointment_schedules_view.dart b/lib/views/appointments/book_appointment_schedules_view.dart new file mode 100644 index 0000000..644b5e6 --- /dev/null +++ b/lib/views/appointments/book_appointment_schedules_view.dart @@ -0,0 +1,9 @@ +import 'package:flutter/material.dart'; +class BookAppointmentSchedulesView extends StatelessWidget { + const BookAppointmentSchedulesView({super.key}); + + @override + Widget build(BuildContext context) { + return const Placeholder(); + } +} diff --git a/lib/views/appointments/book_appointment_services_view.dart b/lib/views/appointments/book_appointment_services_view.dart index 2212426..afe8f67 100644 --- a/lib/views/appointments/book_appointment_services_view.dart +++ b/lib/views/appointments/book_appointment_services_view.dart @@ -62,7 +62,7 @@ class BookAppointmentServicesView extends StatelessWidget { enableDrag: true, builder: (BuildContext context) { return InfoBottomSheet( - title: "Charges Breakdown", + title: "Charges Breakdown".toText(fontSize: 24, isBold: true), description: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -151,7 +151,7 @@ class BookAppointmentServicesView extends StatelessWidget { bgColor: MyColors.white, onTap: () => openTheAddServiceBottomSheet(context, appointmentsVM), text: "Add Services", - icon: Container( + icon: Container( height: 24, width: 24, decoration: const BoxDecoration(shape: BoxShape.circle, color: MyColors.darkTextColor), @@ -161,61 +161,11 @@ class BookAppointmentServicesView extends StatelessWidget { 10.height, ListView.builder( shrinkWrap: true, - itemCount: appointmentsVM.serviceAppointmentScheduleList.length, + itemCount: appointmentsVM.servicesInCurrentAppointment.length, itemBuilder: (BuildContext context, int scheduleIndex) { ServiceAppointmentScheduleModel scheduleData = appointmentsVM.serviceAppointmentScheduleList[scheduleIndex]; return Column( children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: "Schedule: ${scheduleIndex + 1}".toText(fontSize: 20, isBold: true), - ), - Align( - alignment: Alignment.topRight, - child: Icon( - Icons.delete_outline, - size: 28, - ), - ).onPress(() {}), - ], - ), - if (true) ...[ - 5.height, - Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - ("Available Dates").toText(fontSize: 14, isBold: true), - ], - ), - 5.height, - SizedBox( - width: double.infinity, - child: BuildTimeSlots( - timeSlots: scheduleData.availableDates!, - onPressed: (index) => null, - ), - ), - ], - if (scheduleData.selectedDate!.isNotEmpty) ...[ - 5.height, - Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - ("Available Slots").toText(fontSize: 14, isBold: true), - ], - ), - 5.height, - SizedBox( - width: double.infinity, - child: BuildTimeSlots( - timeSlots: _dummySlots, - onPressed: (index) => null, - ), - ), - ], - 20.height, Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -297,7 +247,7 @@ class BookAppointmentServicesView extends StatelessWidget { maxHeight: 55, title: "Review", onPressed: () { - navigateWithName(context, AppRoutes.reviewAppointmentView); + appointmentsVM.onReviewButtonPressed(context); }, backgroundColor: MyColors.darkPrimaryColor, ), diff --git a/lib/views/appointments/book_appointments_item_view.dart b/lib/views/appointments/book_appointments_item_view.dart index 2620166..8f5baeb 100644 --- a/lib/views/appointments/book_appointments_item_view.dart +++ b/lib/views/appointments/book_appointments_item_view.dart @@ -57,18 +57,19 @@ class BookAppointmentsItemView extends StatelessWidget { Divider(), ], ).horPaddingMain(), - appointmentsVM.serviceItems.isEmpty + appointmentsVM.serviceItemsFromApi.isEmpty ? Expanded(child: Center(child: "No Items to show.".toText(fontSize: 16, color: MyColors.lightTextColor))) : ListView.separated( separatorBuilder: (BuildContext context, int index) => Divider(), - itemCount: appointmentsVM.serviceItems.length, + itemCount: appointmentsVM.serviceItemsFromApi.length, itemBuilder: (BuildContext context, int index) { - ItemData itemData = appointmentsVM.serviceItems[index]; + ItemData itemData = appointmentsVM.serviceItemsFromApi[index]; return ServiceItemWithPriceCheckBox( - description: "Some description about the sub-services", + description: itemData.description ?? "Some description about the sub-services", title: itemData.name!, isSelected: itemData.isUpdateOrSelected!, onSelection: (bool value) { + print("itemId: ${itemData.id}"); appointmentsVM.onItemUpdateOrSelected(index, !itemData.isUpdateOrSelected!, itemData.id!); }, priceWidget: Row( @@ -119,17 +120,21 @@ class BookAppointmentsItemView extends StatelessWidget { maxHeight: 55, title: "Next", onPressed: () async { - bool resp = appointmentsVM.onSubServicesNextPressed(); + bool resp = appointmentsVM.validateItemsSelection(); if (resp) { - Utils.showLoading(context); - await appointmentsVM.mergeServiceIntoAvailableSchedules(); - appointmentsVM.resetCategorySelectionBottomSheet(); - Navigator.of(context).pushNamedAndRemoveUntil(AppRoutes.bookAppointmenServicesView, (Route route) => false); - - // appointmentsVM.mergeServiceInAvailableSchedule(); + appointmentsVM.onItemsSelectedInService(); + Navigator.pop(context); } + // bool resp = appointmentsVM.validateItemsSelection(); + // if (resp) { + // Utils.showLoading(context); + // await appointmentsVM.mergeServiceIntoAvailableSchedules(); + // appointmentsVM.resetCategorySelectionBottomSheet(); + // Utils.hideLoading(context); + // Navigator.of(context).pushReplacementNamed(AppRoutes.bookAppointmenServicesView); + // } }, - backgroundColor: !appointmentsVM.isServiceSelectionValidated() ? MyColors.lightTextColor.withOpacity(0.6) : MyColors.primaryColor, + backgroundColor: !appointmentsVM.isServiceSelectionValidated() ? MyColors.lightTextColor.withOpacity(0.6) : MyColors.darkPrimaryColor, ); }, ), diff --git a/lib/views/appointments/review_appointment_view.dart b/lib/views/appointments/review_appointment_view.dart index 841f4c0..c60f84f 100644 --- a/lib/views/appointments/review_appointment_view.dart +++ b/lib/views/appointments/review_appointment_view.dart @@ -96,7 +96,6 @@ class ReviewAppointment extends StatelessWidget { ), ], ), - 10.height, Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -106,21 +105,25 @@ class ReviewAppointment extends StatelessWidget { itemCount: appointmentsVM.servicesInSchedule.length, itemBuilder: (BuildContext context, int serviceIndex) { DropValue selectedService = appointmentsVM.servicesInSchedule[serviceIndex]; + String selectedTimeSlot = ""; + if (scheduleData.selectedCustomTimeDateSlotModel!.availableSlots != null) { + selectedTimeSlot = scheduleData.selectedCustomTimeDateSlotModel!.availableSlots!.firstWhere((element) => element.isSelected).slot; + } return Column( children: [ - Row( - children: [ - Expanded( - child: selectedService.value.toText(fontSize: 15, isBold: true), - ), - ], - ), + // Row( + // children: [ + // Expanded( + // child: selectedService.value.toText(fontSize: 14, isBold: true), + // ), + // ], + // ), if (true) ...[ Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ "Service Location: ".toText(fontSize: 12, color: MyColors.lightTextColor, isBold: true), - ("Home").toText(fontSize: 12, isBold: true).expand(), + (appointmentsVM.isHomeTapped ? "Home" : "Workshop").toText(fontSize: 12, isBold: true).expand(), ], ), 5.height, @@ -130,12 +133,31 @@ class ReviewAppointment extends StatelessWidget { return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - "${itemData.name}: ".toText(fontSize: 13, color: MyColors.lightTextColor, isBold: true), + "${itemData.name}: ${itemData.price} SAR".toText(fontSize: 13, color: MyColors.lightTextColor, isBold: true), ], ); }), ), ], + 5.height, + SizedBox( + width: double.infinity, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "Time & Location".toText(fontSize: 14, isBold: true), + 3.height, + Row(children: [ + "Date & Time: ".toText(fontSize: 12, color: MyColors.lightTextColor, isBold: true), + "${scheduleData.selectedCustomTimeDateSlotModel!.date!.date} at ${selectedTimeSlot}".toText(fontSize: 12, isBold: true), + ]), + Row(children: [ + "Location: ".toText(fontSize: 12, color: MyColors.lightTextColor, isBold: true), + "PM58+F97, Al Olaya, Riyadh 12333".toText(fontSize: 12, isBold: true), + ]), + ], + ), + ) ], ); }, @@ -144,35 +166,9 @@ class ReviewAppointment extends StatelessWidget { ], ), ], - ).toWhiteContainer(width: double.infinity, allPading: 12, margin: const EdgeInsets.symmetric(horizontal: 21, vertical: 10)); + ).toWhiteContainer(width: double.infinity, allPading: 12, margin: const EdgeInsets.symmetric(horizontal: 21, vertical: 0)); }, ); - // AppointmentsVM appointmentsVM = context.read(); - // return SizedBox( - // width: double.infinity, - // child: Column( - // crossAxisAlignment: CrossAxisAlignment.start, - // children: List.generate( - // appointmentsVM.servicesInSchedule.length, - // (serviceIndex) => Column( - // crossAxisAlignment: CrossAxisAlignment.start, - // children: [ - // appointmentsVM.servicesInSchedule[serviceIndex].value.toText(fontSize: 16, isBold: true), - // Column( - // crossAxisAlignment: CrossAxisAlignment.start, - // children: List.generate( - // appointmentsVM, - // (index) => itemList[index].toText( - // fontSize: 12, - // color: MyColors.lightTextColor, - // isBold: true, - // )), - // ), - // ], - // ).paddingOnly(bottom: 14), - // ), - // ).toWhiteContainer(width: double.infinity, allPading: 12, margin: EdgeInsets.symmetric(horizontal: 21)), - // ); } Widget buildTimeAndLocationInfoCard() { @@ -231,25 +227,25 @@ class ReviewAppointment extends StatelessWidget { ), 10.height, Divider(thickness: 0.7), - "Home Location".toText(fontSize: 16, isBold: true), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - "10km ".toText(fontSize: 12, color: MyColors.lightTextColor, isBold: true), - "5 x 10".toText(fontSize: 12, isBold: true), - ], - ), - 8.height, - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - "50 SAR".toText(fontSize: 16, isBold: true), - ], - ), - 10.height, - Divider( - thickness: 0.7, - ), + if (appointmentsVM.isHomeTapped) ...[ + "Home Location".toText(fontSize: 16, isBold: true), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + "10km ".toText(fontSize: 12, color: MyColors.lightTextColor, isBold: true), + "5 x 10".toText(fontSize: 12, isBold: true), + ], + ), + 8.height, + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + "50 SAR".toText(fontSize: 16, isBold: true), + ], + ), + 10.height, + Divider(thickness: 0.7), + ], 10.height, Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -258,7 +254,7 @@ class ReviewAppointment extends StatelessWidget { Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ - "120".toText(fontSize: 29, isBold: true), + totalServicePrice.toString().toText(fontSize: 29, isBold: true), 2.width, "SAR".toText(color: MyColors.lightTextColor, fontSize: 16, isBold: true).paddingOnly(bottom: 5), ], @@ -270,12 +266,13 @@ class ReviewAppointment extends StatelessWidget { ).toWhiteContainer(width: double.infinity, allPading: 12, margin: EdgeInsets.only(left: 21, right: 21, top: 10, bottom: 21)); } - Widget buildNextButtonFooter() { + Widget buildNextButtonFooter({required BuildContext context}) { + AppointmentsVM appointmentsVM = context.read(); return Container( height: 90, color: MyColors.white, child: Column( - mainAxisAlignment: MainAxisAlignment.end, + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Divider(thickness: 0.7, height: 3), 10.height, @@ -283,9 +280,11 @@ class ReviewAppointment extends StatelessWidget { width: double.infinity, child: ShowFillButton( maxHeight: 55, - backgroundColor: MyColors.primaryColor, + backgroundColor: MyColors.darkPrimaryColor, title: "Book Appointment", - onPressed: () {}, + onPressed: () { + // appointmentsVM.onBookAppointmentPressed(); + }, ).paddingOnly(bottom: 12, left: 21, right: 21), ), ], @@ -303,21 +302,18 @@ class ReviewAppointment extends StatelessWidget { actions: [MyAssets.searchIcon.buildSvg().paddingOnly(right: 21)], onBackButtonTapped: () => Navigator.pop(context), ), - body: Stack( - alignment: Alignment.bottomCenter, + body: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - buildBranchInfoCard(context: context), - buildServicesInfoCard(context: context), - buildTimeAndLocationInfoCard(), - buildChargesBreakDown(context: context), - ], - ), - ), - buildNextButtonFooter(), + ListView( + children: [ + buildBranchInfoCard(context: context), + buildServicesInfoCard(context: context), + // buildTimeAndLocationInfoCard(), + buildChargesBreakDown(context: context), + ], + ).expand(), + buildNextButtonFooter(context: context), ], ), ); diff --git a/lib/views/appointments/widgets/appointment_service_pick_bottom_sheet.dart b/lib/views/appointments/widgets/appointment_service_pick_bottom_sheet.dart index 4bab6e6..a13af5d 100644 --- a/lib/views/appointments/widgets/appointment_service_pick_bottom_sheet.dart +++ b/lib/views/appointments/widgets/appointment_service_pick_bottom_sheet.dart @@ -174,12 +174,13 @@ class AppointmentServicePickBottomSheet extends StatelessWidget { width: double.infinity, child: ShowFillButton( maxHeight: 55, - backgroundColor: !appointmentsVM.isServiceSelectionValidated() ? MyColors.lightTextColor.withOpacity(0.6) : MyColors.primaryColor, + backgroundColor: !appointmentsVM.isServiceSelectionValidated() ? MyColors.lightTextColor.withOpacity(0.6) : MyColors.darkPrimaryColor, title: "Next", onPressed: () { bool isValidated = appointmentsVM.isServiceSelectionValidated(); if (isValidated) { appointmentsVM.getServiceItems(appointmentsVM.branchSelectedServiceId.selectedId); + Navigator.pop(context); navigateWithName(context, AppRoutes.bookAppointmentsItemView); } }, diff --git a/lib/views/provider/branch_detail_page.dart b/lib/views/provider/branch_detail_page.dart index 34092a3..e49cf16 100644 --- a/lib/views/provider/branch_detail_page.dart +++ b/lib/views/provider/branch_detail_page.dart @@ -178,7 +178,10 @@ class _BranchDetailPageState extends State { showItem("Home service range", widget.branchDetailModel.branchServices![index].customerLocationRange.toString() + "KM"), showItem("Charges per KM", widget.branchDetailModel.branchServices![index].customerLocationRange.toString() + "SAR"), 8.height, - (widget.branchDetailModel.branchServices![index].itemsCount.toString() + "+ items").toText( + ((widget.branchDetailModel.branchServices![index].itemsCount != null && widget.branchDetailModel.branchServices![index].itemsCount! > 0) + ? widget.branchDetailModel.branchServices![index].itemsCount.toString() + " items" + : "No" + " items") + .toText( fontSize: 12, isBold: true, color: MyColors.primaryColor, @@ -187,7 +190,9 @@ class _BranchDetailPageState extends State { 20.height, ], ).onPress(() { - showMyBottomSheet(context, child: ItemsListSheet(widget.branchDetailModel.branchServices![index].serviceProviderServiceId ?? 0)); + if (widget.branchDetailModel.branchServices![index].itemsCount != null && widget.branchDetailModel.branchServices![index].itemsCount! > 0) { + showMyBottomSheet(context, child: ItemsListSheet(widget.branchDetailModel.branchServices![index].serviceProviderServiceId ?? 0)); + } }), ], onExpansionChanged: (value) { diff --git a/lib/views/provider/sheet/items_list_sheet.dart b/lib/views/provider/sheet/items_list_sheet.dart index 4738e2d..0525c79 100644 --- a/lib/views/provider/sheet/items_list_sheet.dart +++ b/lib/views/provider/sheet/items_list_sheet.dart @@ -30,12 +30,12 @@ class _ItemsListSheetState extends State { height: MediaQuery.of(context).size.height / 1.2, child: Consumer( builder: (context, appointmentsVM, _) { - return appointmentsVM.serviceItems.isEmpty + return appointmentsVM.serviceItemsFromApi.isEmpty ? const EmptyWidget() : ListView.separated( - itemCount: appointmentsVM.serviceItems.length, + itemCount: appointmentsVM.serviceItemsFromApi.length, itemBuilder: (BuildContext context, int index) { - ItemData serviceItemModel = appointmentsVM.serviceItems[index]; + ItemData serviceItemModel = appointmentsVM.serviceItemsFromApi[index]; return SizedBox( width: double.infinity, child: Row( diff --git a/pubspec.yaml b/pubspec.yaml index 3c4aa16..a7d4453 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -18,7 +18,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev version: 1.0.0+1 environment: - sdk: ">=2.12.0 <3.0.0" + sdk: ">=2.17.0 <3.0.0" # Dependencies specify other packages that your package needs in order to work. # To automatically upgrade your package dependencies to the latest versions From 120dc86309a301b3320eae6965a9b08dd70cc4b6 Mon Sep 17 00:00:00 2001 From: Faiz Hashmi Date: Thu, 19 Oct 2023 17:15:24 +0300 Subject: [PATCH 2/5] aDDED cUSTOM cALENDER --- lib/config/customer_routes.dart | 2 + lib/main.dart | 72 ++-- lib/repositories/schedule_repo.dart | 24 +- lib/view_models/appointments_view_model.dart | 341 ++++++++++++------ .../book_appointment_schedules_view.dart | 155 +++++++- .../book_appointment_services_view.dart | 221 +++--------- .../appointments/review_appointment_view.dart | 20 +- .../widgets/custom_calender_widget.dart | 215 +++++++++++ lib/views/provider/branch_detail_page.dart | 2 +- .../provider/sheet/items_list_sheet.dart | 2 +- pubspec.yaml | 1 + 11 files changed, 716 insertions(+), 339 deletions(-) create mode 100644 lib/views/appointments/widgets/custom_calender_widget.dart diff --git a/lib/config/customer_routes.dart b/lib/config/customer_routes.dart index e2823e5..5075082 100644 --- a/lib/config/customer_routes.dart +++ b/lib/config/customer_routes.dart @@ -1,4 +1,5 @@ import 'package:car_customer_app/views/appointments/appointment_detail_view.dart'; +import 'package:car_customer_app/views/appointments/book_appointment_schedules_view.dart'; import 'package:car_customer_app/views/appointments/book_appointment_services_view.dart'; import 'package:car_customer_app/views/appointments/book_appointments_item_view.dart'; import 'package:car_customer_app/views/appointments/pick_items_view.dart'; @@ -28,6 +29,7 @@ class CustomerAppRoutes { AppRoutes.adsSearchFilterScreen: (context) => AdsSearchFilterView(), AppRoutes.selectAdTypeView: (context) => SelectAdTypeView(isProvider: ModalRoute.of(context)!.settings.arguments as bool), AppRoutes.bookAppointmenServicesView: (context) => BookAppointmentServicesView(), + AppRoutes.bookAppointmenSchedulesView: (context) => BookAppointmentSchedulesView(), AppRoutes.bookAppointmentsItemView: (context) => BookAppointmentsItemView(), AppRoutes.reviewAppointmentView: (context) => ReviewAppointment(), AppRoutes.paymentMethodsView: (context) => PaymentMethodsView(paymentType: ModalRoute.of(context)!.settings.arguments as PaymentTypes), diff --git a/lib/main.dart b/lib/main.dart index 966f089..bd7ca33 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -26,6 +26,7 @@ import 'package:mc_common_app/widgets/extensions/extensions_widget.dart'; import 'package:provider/provider.dart'; import 'package:provider/single_child_widget.dart'; import 'package:sizer/sizer.dart'; +import 'package:intl/date_symbol_data_local.dart'; //test commit Future main() async { @@ -33,42 +34,41 @@ Future main() async { CustomerDependencies.addDependencies(); await EasyLocalization.ensureInitialized(); CustomerAppRoutes.routes.addAll(AppRoutes.routes); - - runApp( - MultiProvider( - providers: [ - ChangeNotifierProvider(create: (_) => BaseVM()), - ChangeNotifierProvider( - create: (_) => DashboardVM( - commonServices: injector.get(), - userRepo: injector.get(), - ), - ), - ChangeNotifierProvider( - create: (_) => UserVM(userRepo: injector.get()), - ), - ChangeNotifierProvider( - create: (_) => AdVM( - commonServices: injector.get(), - commonRepo: injector.get(), - adsRepo: injector.get(), - ), - ), - ChangeNotifierProvider( - create: (_) => AppointmentsVM( - scheduleRepo: injector.get(), - providerRepo: injector.get(), - commonServices: injector.get(), - commonRepo: injector.get(), - ), - ), - ChangeNotifierProvider( - create: (_) => PaymentVM(paymentService: injector.get(), paymentRepo: injector.get()), - ), - ], - child: const MyApp(), - ).setupLocale(), - ); + initializeDateFormatting().then((_) => runApp( + MultiProvider( + providers: [ + ChangeNotifierProvider(create: (_) => BaseVM()), + ChangeNotifierProvider( + create: (_) => DashboardVM( + commonServices: injector.get(), + userRepo: injector.get(), + ), + ), + ChangeNotifierProvider( + create: (_) => UserVM(userRepo: injector.get()), + ), + ChangeNotifierProvider( + create: (_) => AdVM( + commonServices: injector.get(), + commonRepo: injector.get(), + adsRepo: injector.get(), + ), + ), + ChangeNotifierProvider( + create: (_) => AppointmentsVM( + scheduleRepo: injector.get(), + providerRepo: injector.get(), + commonServices: injector.get(), + commonRepo: injector.get(), + ), + ), + ChangeNotifierProvider( + create: (_) => PaymentVM(paymentService: injector.get(), paymentRepo: injector.get()), + ), + ], + child: MyApp(), + ).setupLocale(), + )); } // todo terminal command to generate translation files diff --git a/lib/repositories/schedule_repo.dart b/lib/repositories/schedule_repo.dart index 8fbe9e3..3ea0083 100644 --- a/lib/repositories/schedule_repo.dart +++ b/lib/repositories/schedule_repo.dart @@ -21,7 +21,10 @@ abstract class ScheduleRepo { Future updateServicesInSchedule(Map map); - Future> mergeServiceIntoAvailableSchedules({required int appointmentType, required List serviceItemIds}); + Future> mergeServiceIntoAvailableSchedules({ + required List serviceItemIdsForHome, + required List serviceItemIdsForWorkshop, + }); Future createServiceAppointment({required List serviceItemIds, required int serviceSlotID}); } @@ -73,12 +76,21 @@ class ScheduleRepoImp implements ScheduleRepo { return await injector.get().postJsonForObject((json) => MResponse.fromJson(json), ApiConsts.updateGroup, map, token: t); } - Future> mergeServiceIntoAvailableSchedules({required int appointmentType, required List serviceItemIds}) async { + Future> mergeServiceIntoAvailableSchedules({ + required List serviceItemIdsForHome, + required List serviceItemIdsForWorkshop, + }) async { String t = AppState().getUser.data!.accessToken ?? ""; - var queryParameters = { - "appointmentType": appointmentType, - "ServiceItemIDs": serviceItemIds, - }; + var queryParameters = [ + { + "appointmentType": 2, + "ServiceItemIDs": serviceItemIdsForHome, + }, + { + "appointmentType": 1, + "ServiceItemIDs": serviceItemIdsForWorkshop, + } + ]; GenericRespModel adsGenericModel = await injector.get().postJsonForObject( (json) => GenericRespModel.fromJson(json), ApiConsts.GetServiceItemAppointmentScheduleSlots, diff --git a/lib/view_models/appointments_view_model.dart b/lib/view_models/appointments_view_model.dart index f3b3deb..e5a4d38 100644 --- a/lib/view_models/appointments_view_model.dart +++ b/lib/view_models/appointments_view_model.dart @@ -1,8 +1,14 @@ +import 'dart:developer'; + import 'package:car_customer_app/repositories/provider_repo.dart'; import 'package:car_customer_app/repositories/schedule_repo.dart'; +import 'package:car_customer_app/views/appointments/widgets/appointment_service_pick_bottom_sheet.dart'; import 'package:flutter/cupertino.dart'; +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/provider_branches_models/branch_detail_model.dart'; import 'package:mc_common_app/models/provider_branches_models/provider_profile_model.dart'; @@ -12,11 +18,14 @@ 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/common_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/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'; class AppointmentsVM extends BaseVM { final CommonRepo commonRepo; @@ -49,45 +58,39 @@ class AppointmentsVM extends BaseVM { List servicesInSchedule = []; - Future mergeServiceIntoAvailableSchedules() async { - List serviceItemIds = []; - currentServiceSelection!.serviceItems!.forEach((serviceItem) { - if (serviceItem.isUpdateOrSelected!) { - serviceItemIds.add(serviceItem.id!.toString()); - } - }); - - servicesInCurrentAppointment.add(currentServiceSelection!); - - serviceAppointmentScheduleList = await scheduleRepo.mergeServiceIntoAvailableSchedules( - serviceItemIds: serviceItemIds, - appointmentType: isHomeTapped ? 2 : 1, // AppointmentType 1 for workshop and 2 for Home based schedule. - ); - - servicesInSchedule.clear(); - - if (serviceAppointmentScheduleList.isEmpty) { - Utils.showToast("There are no available appointments for selected Items."); - return; + bool ifServiceAlreadyThere(int id) { + int index = servicesInSchedule.indexWhere((element) => element.id == id); + if (index == -1) { + return false; } + return true; + } - serviceAppointmentScheduleList.forEach( - (schedule) { - amountToPayForAppointment = amountToPayForAppointment + (schedule.amountToPay ?? 0.0); - - schedule.serviceItemList!.forEach((item) { - if (!ifItemAlreadyThere(item.serviceProviderServiceId!)) { - servicesInSchedule.add(DropValue(item.serviceProviderServiceId!, item.description!, "")); - } - }); - }, - ); - notifyListeners(); + 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) { - servicesInCurrentAppointment.add(currentServiceSelection!); + 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(); } } @@ -96,9 +99,9 @@ class AppointmentsVM extends BaseVM { try { serviceAppointmentScheduleList.forEach((schedule) async { List serviceItemIds = []; - schedule.serviceItemList!.forEach((serviceItem) { - serviceItemIds.add(serviceItem.id.toString()); - }); + // schedule.serviceItemList!.forEach((serviceItem) { + // serviceItemIds.add(serviceItem.id.toString()); + // }); await scheduleRepo.createServiceAppointment(serviceItemIds: serviceItemIds, serviceSlotID: schedule.selectedCustomTimeDateSlotModel!.date!.slotId); }); } catch (e) { @@ -106,16 +109,11 @@ class AppointmentsVM extends BaseVM { } } - bool ifItemAlreadyThere(int id) { - int index = servicesInSchedule.indexWhere((element) => element.id == id); - if (index == -1) { - return false; - } - return true; - } - void updateIsHomeTapped(bool value) { isHomeTapped = value; + if (currentServiceSelection != null) { + currentServiceSelection!.isHomeSelected = value; + } notifyListeners(); } @@ -124,6 +122,9 @@ class AppointmentsVM extends BaseVM { void updatePickedHomeLocation(String value) { pickedHomeLocation = value; pickHomeLocationError = ""; + if (currentServiceSelection != null) { + currentServiceSelection!.homeLocation = value; + } notifyListeners(); } @@ -217,67 +218,43 @@ class AppointmentsVM extends BaseVM { int index = serviceAppointmentScheduleList[scheduleIndex].selectedDateIndex!; serviceAppointmentScheduleList[scheduleIndex].customTimeDateSlotList![index].availableSlots![slotIndex].isSelected = true; serviceAppointmentScheduleList[scheduleIndex].selectedCustomTimeDateSlotModel!.availableSlots = serviceAppointmentScheduleList[scheduleIndex].customTimeDateSlotList![index].availableSlots!; - print("here: ${serviceAppointmentScheduleList[scheduleIndex].customTimeDateSlotList![index].availableSlots![slotIndex].slotId}"); notifyListeners(); } double amountToPayForAppointment = 0.0; double totalAmount = 0.0; - 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); - } - List serviceItemsFromApi = []; + ProviderProfileModel? providerProfileModel; int selectedSubServicesCounter = 0; - updateSelectedSubServicesCounter(int value) { - selectedSubServicesCounter = value; - notifyListeners(); - } - onItemUpdateOrSelected(int index, bool selected, int itemId) { + int serviceIndex = servicesInCurrentAppointment.indexWhere((element) => element.serviceId == currentServiceSelection!.serviceId!); + serviceItemsFromApi[index].isUpdateOrSelected = selected; + serviceItemsFromApi[index].isHomeSelected = isHomeTapped; if (selected) { selectedSubServicesCounter = selectedSubServicesCounter + 1; - updateSelectedSubServicesCounter(selectedSubServicesCounter); 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; - updateSelectedSubServicesCounter(selectedSubServicesCounter); 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(); } @@ -315,6 +292,11 @@ class AppointmentsVM extends BaseVM { 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; } @@ -396,26 +378,173 @@ class AppointmentsVM extends BaseVM { return false; } -// void mergeServiceInAvailableSchedule() { -// log("schedules: ${availableSchedules}"); -// for (var schedule in availableSchedules) { -// for (var service in schedule.scheduleServices!) { -// if (branchSelectedServiceId.selectedId == service.serviceId) { -// log("ID matched: ${service.serviceId}"); -// log("SelectedServices: ${schedule.selectedServices}"); -// -// -// int isAlreadyThereIndex = schedule.selectedServices!.indexWhere((element) => element.serviceProviderServiceId == branchSelectedServiceId.selectedId); -// if (isAlreadyThereIndex != -1) { -// log("removing: ${service.serviceId}"); -// schedule.selectedServices!.removeAt(isAlreadyThereIndex); -// } -// schedule.selectedServices!.add(currentServiceSelection!); -// notifyListeners(); -// return; -// } -// } -// } -// notifyListeners(); -// } + 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 ?? 0.0) + (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()); + } + }); + + servicesInSchedule.clear(); + + //TODO: WE HAVE TO ADD PARAMETER IN THE allSelectedItemsInAppointments TO DECIDE WHETHER THE APPOINTMENT IS FOR HOME OR WORKSHOP + + 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; + } + + serviceAppointmentScheduleList.forEach( + (schedule) { + amountToPayForAppointment = amountToPayForAppointment + (schedule.amountToPay ?? 0.0); + + schedule.servicesListInAppointment!.forEach((service) { + if (!ifServiceAlreadyThere(service.serviceProviderServiceId!)) { + servicesInSchedule.add(DropValue(service.serviceProviderServiceId!, service.providerServiceDescription!, "")); + } + }); + }, + ); + Utils.hideLoading(context); + + navigateWithName(context, AppRoutes.bookAppointmenSchedulesView); + + notifyListeners(); + } } diff --git a/lib/views/appointments/book_appointment_schedules_view.dart b/lib/views/appointments/book_appointment_schedules_view.dart index 644b5e6..8b37974 100644 --- a/lib/views/appointments/book_appointment_schedules_view.dart +++ b/lib/views/appointments/book_appointment_schedules_view.dart @@ -1,9 +1,160 @@ +import 'package:car_customer_app/view_models/appointments_view_model.dart'; +import 'package:car_customer_app/views/appointments/widgets/appointment_service_pick_bottom_sheet.dart'; +import 'package:car_customer_app/views/appointments/widgets/custom_calender_widget.dart'; 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/service_schedule_model.dart'; +import 'package:mc_common_app/models/services/item_model.dart'; +import 'package:mc_common_app/models/widgets_models.dart'; +import 'package:mc_common_app/theme/colors.dart'; +import 'package:mc_common_app/utils/navigator.dart'; +import 'package:mc_common_app/views/advertisement/custom_add_button.dart'; +import 'package:mc_common_app/widgets/button/show_fill_button.dart'; +import 'package:mc_common_app/widgets/common_widgets/app_bar.dart'; +import 'package:mc_common_app/widgets/common_widgets/info_bottom_sheet.dart'; +import 'package:mc_common_app/widgets/common_widgets/time_slots.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 BookAppointmentSchedulesView extends StatelessWidget { - const BookAppointmentSchedulesView({super.key}); + BookAppointmentSchedulesView({Key? key}) : super(key: key); @override Widget build(BuildContext context) { - return const Placeholder(); + return Scaffold( + appBar: CustomAppBar( + title: "Book Appointment", + isRemoveBackButton: false, + isDrawerEnabled: false, + actions: [MyAssets.searchIcon.buildSvg().paddingOnly(right: 21)], + onBackButtonTapped: () => Navigator.pop(context), + ), + body: Consumer( + builder: (BuildContext context, AppointmentsVM appointmentsVM, Widget? child) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + 21.height, + ListView.builder( + shrinkWrap: true, + itemCount: appointmentsVM.serviceAppointmentScheduleList.length, + itemBuilder: (BuildContext context, int scheduleIndex) { + ServiceAppointmentScheduleModel scheduleData = appointmentsVM.serviceAppointmentScheduleList[scheduleIndex]; + return ExpansionTile( + tilePadding: EdgeInsets.symmetric(horizontal: 21, vertical: 10), + childrenPadding: EdgeInsets.only(left: 16, bottom: 10, right: 16), + title: Column( + children: [ + Row( + children: [ + Expanded( + child: "Schedule ${scheduleIndex + 1}".toText(fontSize: 20, isBold: true), + ), + ], + ), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "Service Location: ".toText(fontSize: 12, color: MyColors.lightTextColor, isBold: true), + ("${scheduleData.appointmentType == 2 ? "Home" : "Workshop"}").toText(fontSize: 12, isBold: true).expand(), + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + 5.height, + ListView.builder( + physics: NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: appointmentsVM.servicesInSchedule.length, + itemBuilder: (BuildContext context, int serviceIndex) { + DropValue selectedService = appointmentsVM.servicesInSchedule[serviceIndex]; + return Row( + children: [ + Expanded( + child: ("${serviceIndex + 1}. ${selectedService.value}").toText(fontSize: 15, isBold: true, color: MyColors.lightTextColor), + ), + ], + ); + }, + ), + ], + ), + ], + ), + children: [ + Column( + children: [ + if (true) ...[ + // SizedBox( + // width: double.infinity, + // child: BuildDateSlotsForAppointment( + // customDateSlots: scheduleData.customTimeDateSlotList ?? [], + // onPressed: (dateIndex) { + // appointmentsVM.updateSelectedAppointmentDate(scheduleIndex: scheduleIndex, dateIndex: dateIndex); + // }, + // ), + // ), + CustomCalenderWidget(customTimeDateSlotList: scheduleData.customTimeDateSlotList ?? []), + ], + if (appointmentsVM.serviceAppointmentScheduleList[scheduleIndex].selectedDateIndex != null) ...[ + 5.height, + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + ("Available Slots").toText(fontSize: 14, isBold: true), + ], + ), + 5.height, + SizedBox( + width: double.infinity, + child: BuildTimeSlots( + timeSlots: appointmentsVM.serviceAppointmentScheduleList[scheduleIndex] + .customTimeDateSlotList![appointmentsVM.serviceAppointmentScheduleList[scheduleIndex].selectedDateIndex!].availableSlots ?? + [], + onPressed: (slotIndex) { + appointmentsVM.updateSelectedAppointmentSlotByDate(scheduleIndex: scheduleIndex, slotIndex: slotIndex); + }, + ), + ), + ], + ], + ), + ], + ).toWhiteContainer(width: double.infinity, margin: const EdgeInsets.symmetric(horizontal: 21, vertical: 10)); + }, + ).expand(), + Row( + children: [ + Expanded( + child: ShowFillButton( + txtColor: MyColors.black, + maxHeight: 55, + title: "Cancel", + onPressed: () {}, + backgroundColor: MyColors.greyButtonColor, + ), + ), + 12.width, + Expanded( + child: ShowFillButton( + maxHeight: 55, + title: "Review", + onPressed: () { + appointmentsVM.onReviewButtonPressed(context); + }, + backgroundColor: MyColors.darkPrimaryColor, + ), + ) + ], + ).paddingAll(21) + ], + ); + }, + )); } } diff --git a/lib/views/appointments/book_appointment_services_view.dart b/lib/views/appointments/book_appointment_services_view.dart index afe8f67..cad86a8 100644 --- a/lib/views/appointments/book_appointment_services_view.dart +++ b/lib/views/appointments/book_appointment_services_view.dart @@ -1,135 +1,20 @@ import 'package:car_customer_app/view_models/appointments_view_model.dart'; -import 'package:car_customer_app/views/appointments/widgets/appointment_service_pick_bottom_sheet.dart'; 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/service_schedule_model.dart'; import 'package:mc_common_app/models/services/item_model.dart'; -import 'package:mc_common_app/models/widgets_models.dart'; +import 'package:mc_common_app/models/services/service_model.dart'; import 'package:mc_common_app/theme/colors.dart'; -import 'package:mc_common_app/utils/navigator.dart'; import 'package:mc_common_app/views/advertisement/custom_add_button.dart'; import 'package:mc_common_app/widgets/button/show_fill_button.dart'; import 'package:mc_common_app/widgets/common_widgets/app_bar.dart'; -import 'package:mc_common_app/widgets/common_widgets/info_bottom_sheet.dart'; -import 'package:mc_common_app/widgets/common_widgets/time_slots.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 BookAppointmentServicesView extends StatelessWidget { BookAppointmentServicesView({Key? key}) : super(key: key); - void openTheAddServiceBottomSheet(BuildContext context, AppointmentsVM appointmentsVM) { - showModalBottomSheet( - context: context, - isScrollControlled: true, - enableDrag: true, - builder: (BuildContext context) { - return AppointmentServicePickBottomSheet(); - }, - ); - } - - final _dummySlots = [ - TimeSlotModel(isSelected: false, slotId: 1, slot: "11:00"), - TimeSlotModel(isSelected: false, slotId: 2, slot: "12:00"), - TimeSlotModel(isSelected: true, slotId: 3, slot: "13:00"), - TimeSlotModel(isSelected: false, slotId: 4, slot: "14:00"), - TimeSlotModel(isSelected: false, slotId: 5, slot: "15:00"), - TimeSlotModel(isSelected: false, slotId: 6, slot: "16:00"), - TimeSlotModel(isSelected: false, slotId: 7, slot: "17:00"), - TimeSlotModel(isSelected: false, slotId: 8, slot: "18:00"), - TimeSlotModel(isSelected: false, slotId: 9, slot: "19:00"), - TimeSlotModel(isSelected: false, slotId: 10, slot: "20:00"), - TimeSlotModel(isSelected: false, slotId: 11, slot: "21:00"), - TimeSlotModel(isSelected: false, slotId: 12, slot: "22:00"), - TimeSlotModel(isSelected: false, slotId: 13, slot: "23:00"), - TimeSlotModel(isSelected: false, slotId: 14, slot: "24:00"), - TimeSlotModel(isSelected: false, slotId: 15, slot: "25:00"), - TimeSlotModel(isSelected: false, slotId: 16, slot: "26:00"), - TimeSlotModel(isSelected: false, slotId: 17, slot: "27:00"), - TimeSlotModel(isSelected: false, slotId: 18, slot: "28:00"), - TimeSlotModel(isSelected: false, slotId: 19, slot: "29:00"), - ]; - - void priceBreakDownClicked(BuildContext context) { - showModalBottomSheet( - context: context, - isScrollControlled: true, - enableDrag: true, - builder: (BuildContext context) { - 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( - 5, - (index) => Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - "Gear Kit".toText(fontSize: 12, color: MyColors.lightTextColor, isBold: true), - "200 SAR".toText(fontSize: 12, isBold: true), - ], - ), - ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - "1149 SAR".toText(fontSize: 16, isBold: true), - ], - ), - 20.height, - "Home Location".toText(fontSize: 16, isBold: true), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - "10km ".toText(fontSize: 12, color: MyColors.lightTextColor, isBold: true), - "5 x 10".toText(fontSize: 12, isBold: true), - ], - ), - 8.height, - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - "50 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: [ - "120".toText(fontSize: 29, isBold: true), - 2.width, - "SAR".toText(color: MyColors.lightTextColor, fontSize: 16, isBold: true).paddingOnly(bottom: 5), - ], - ) - ], - ), - 30.height, - ], - )); - }); - } - - String getTotalPrice(List serviceItems) { - var totalPrice = 0.0; - serviceItems.forEach((element) { - totalPrice = totalPrice + double.parse(element.price ?? "0.0"); - }); - return totalPrice.toString(); - } - @override Widget build(BuildContext context) { return Scaffold( @@ -149,7 +34,7 @@ class BookAppointmentServicesView extends StatelessWidget { CustomAddButton( needsBorder: true, bgColor: MyColors.white, - onTap: () => openTheAddServiceBottomSheet(context, appointmentsVM), + onTap: () => appointmentsVM.openTheAddServiceBottomSheet(context, appointmentsVM), text: "Add Services", icon: Container( height: 24, @@ -162,70 +47,52 @@ class BookAppointmentServicesView extends StatelessWidget { ListView.builder( shrinkWrap: true, itemCount: appointmentsVM.servicesInCurrentAppointment.length, - itemBuilder: (BuildContext context, int scheduleIndex) { - ServiceAppointmentScheduleModel scheduleData = appointmentsVM.serviceAppointmentScheduleList[scheduleIndex]; + itemBuilder: (BuildContext context, int serviceIndex) { + ServiceModel serviceData = appointmentsVM.servicesInCurrentAppointment[serviceIndex]; return Column( children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, + Row( children: [ - ListView.separated( - physics: NeverScrollableScrollPhysics(), - shrinkWrap: true, - itemCount: appointmentsVM.servicesInSchedule.length, - itemBuilder: (BuildContext context, int serviceIndex) { - DropValue selectedService = appointmentsVM.servicesInSchedule[serviceIndex]; - return Column( - children: [ - Row( - children: [ - Expanded( - child: selectedService.value.toText(fontSize: 15, isBold: true), - ), - ], - ), - if (true) ...[ - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - "Service Location: ".toText(fontSize: 12, color: MyColors.lightTextColor, isBold: true), - ("Home").toText(fontSize: 12, isBold: true).expand(), - ], - ), - 5.height, - Column( - children: List.generate(scheduleData.serviceItemList!.length, (itemIndex) { - ItemData itemData = scheduleData.serviceItemList![itemIndex]; - return Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - "${itemData.name}: ".toText(fontSize: 13, color: MyColors.lightTextColor, isBold: true), - ("${itemData.price}").toText(fontSize: 13, isBold: true).expand(), - ], - ); - }), - ), - ], - ], - ); - }, - separatorBuilder: (BuildContext context, int index) => Divider(thickness: 2), + Expanded( + child: (serviceData.serviceDescription ?? "").toText(fontSize: 15, isBold: true), ), - 8.height, - Row( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - getTotalPrice(appointmentsVM.serviceAppointmentScheduleList[scheduleIndex].serviceItemList ?? []).toText(fontSize: 32, isBold: true), - 2.width, - "SAR".toText(color: MyColors.lightTextColor, fontSize: 16, isBold: true).paddingOnly(bottom: 5), - Icon( - Icons.arrow_drop_down, - size: 30, - ) - ], - ).onPress(() => priceBreakDownClicked(context)), ], ), + if (true) ...[ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "Service Location: ".toText(fontSize: 12, color: MyColors.lightTextColor, isBold: true), + (serviceData.isHomeSelected ? serviceData.homeLocation : "Workshop").toText(fontSize: 12, isBold: true).expand(), + ], + ), + 5.height, + Column( + children: List.generate(serviceData.serviceItems!.length, (itemIndex) { + ItemData itemData = serviceData.serviceItems![itemIndex]; + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "${itemData.name}: ".toText(fontSize: 13, color: MyColors.lightTextColor, isBold: true), + ("${itemData.price}").toText(fontSize: 13, isBold: true).expand(), + ], + ); + }), + ), + 8.height, + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + ((appointmentsVM.servicesInCurrentAppointment[serviceIndex].currentTotalServicePrice).toString()).toText(fontSize: 32, isBold: true), + 2.width, + "SAR".toText(color: MyColors.lightTextColor, fontSize: 16, isBold: true).paddingOnly(bottom: 5), + Icon( + Icons.arrow_drop_down, + size: 30, + ) + ], + ).onPress(() => appointmentsVM.priceBreakDownClicked(context, appointmentsVM.servicesInCurrentAppointment[serviceIndex])), + ], ], ).toWhiteContainer(width: double.infinity, allPading: 12, margin: const EdgeInsets.symmetric(horizontal: 21, vertical: 10)); }, @@ -245,9 +112,9 @@ class BookAppointmentServicesView extends StatelessWidget { Expanded( child: ShowFillButton( maxHeight: 55, - title: "Review", + title: "Next", onPressed: () { - appointmentsVM.onReviewButtonPressed(context); + appointmentsVM.onServicesNextPressed(context); }, backgroundColor: MyColors.darkPrimaryColor, ), diff --git a/lib/views/appointments/review_appointment_view.dart b/lib/views/appointments/review_appointment_view.dart index c60f84f..5862e51 100644 --- a/lib/views/appointments/review_appointment_view.dart +++ b/lib/views/appointments/review_appointment_view.dart @@ -7,6 +7,7 @@ import 'package:mc_common_app/extensions/string_extensions.dart'; import 'package:mc_common_app/generated/locale_keys.g.dart'; import 'package:mc_common_app/models/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/theme/colors.dart'; import 'package:mc_common_app/widgets/button/show_fill_button.dart'; import 'package:mc_common_app/widgets/common_widgets/app_bar.dart'; @@ -104,7 +105,6 @@ class ReviewAppointment extends StatelessWidget { shrinkWrap: true, itemCount: appointmentsVM.servicesInSchedule.length, itemBuilder: (BuildContext context, int serviceIndex) { - DropValue selectedService = appointmentsVM.servicesInSchedule[serviceIndex]; String selectedTimeSlot = ""; if (scheduleData.selectedCustomTimeDateSlotModel!.availableSlots != null) { selectedTimeSlot = scheduleData.selectedCustomTimeDateSlotModel!.availableSlots!.firstWhere((element) => element.isSelected).slot; @@ -128,12 +128,12 @@ class ReviewAppointment extends StatelessWidget { ), 5.height, Column( - children: List.generate(scheduleData.serviceItemList!.length, (itemIndex) { - ItemData itemData = scheduleData.serviceItemList![itemIndex]; + children: List.generate(scheduleData.servicesListInAppointment!.length, (itemIndex) { + ServiceModel serviceData = scheduleData.servicesListInAppointment![itemIndex]; return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - "${itemData.name}: ${itemData.price} SAR".toText(fontSize: 13, color: MyColors.lightTextColor, isBold: true), + "${serviceData.providerServiceDescription}".toText(fontSize: 13, color: MyColors.lightTextColor, isBold: true), ], ); }), @@ -196,12 +196,12 @@ class ReviewAppointment extends StatelessWidget { AppointmentsVM appointmentsVM = context.read(); List allSelectedItems = []; double totalServicePrice = 0.0; - appointmentsVM.serviceAppointmentScheduleList.forEach((schedule) { - schedule.serviceItemList!.forEach((item) { - allSelectedItems.add(item); - totalServicePrice = totalServicePrice + double.parse(item.price!); - }); - }); + // appointmentsVM.serviceAppointmentScheduleList.forEach((schedule) { + // schedule.servicesListInAppointment!.forEach((service) { + // allSelectedItems.add(item); + // totalServicePrice = totalServicePrice + double.parse(item.price!); + // }); + // }); return Column( crossAxisAlignment: CrossAxisAlignment.start, diff --git a/lib/views/appointments/widgets/custom_calender_widget.dart b/lib/views/appointments/widgets/custom_calender_widget.dart new file mode 100644 index 0000000..7b86c0d --- /dev/null +++ b/lib/views/appointments/widgets/custom_calender_widget.dart @@ -0,0 +1,215 @@ +import 'dart:developer'; + +import 'package:car_customer_app/view_models/appointments_view_model.dart'; +import 'package:flutter/material.dart'; +import 'package:mc_common_app/extensions/int_extensions.dart'; +import 'package:mc_common_app/extensions/string_extensions.dart'; +import 'package:mc_common_app/models/service_schedule_model.dart'; +import 'package:mc_common_app/theme/colors.dart'; +import 'package:mc_common_app/widgets/dropdown/dropdow_field.dart'; +import 'package:mc_common_app/widgets/extensions/extensions_widget.dart'; +import 'package:table_calendar/table_calendar.dart'; +import 'package:provider/provider.dart'; +import 'package:intl/intl.dart'; + +class CustomCalenderWidget extends StatefulWidget { + final List? customTimeDateSlotList; + + const CustomCalenderWidget({super.key, required this.customTimeDateSlotList}); + + @override + State createState() => _CustomCalenderWidgetState(); +} + +class _CustomCalenderWidgetState extends State { + List allDates = []; + List allMonths = []; + List datesInSelectedMonth = []; + CalendarFormat _calendarFormat = CalendarFormat.month; + + late int selectedMonth; + late int selectedYear; + + DateTime? _selectedDay; + + DateTime _focusedDay = DateTime.now(); + + @override + void initState() { + super.initState(); + populateDateList(); + } + + populateDateList() { + for (var value in widget.customTimeDateSlotList!) { + DateTime dt = DateFormat('dd MMMM, yyyy').parse(value.date!.date); + // TODO: THIS COMPARISON NEEDS TO BE FIXED!!!!! + // TODO: BECAUSE WE CAN ONLY VALUE IN DROPDOWN, SO SUBVALUE IS OF NO USE!! + + DropValue dv = DropValue(dt.month, "${dt.month.getMonthNameByNumber()}, ${dt.year}", ""); + allDates.add(dt); + + if (!ifMonthAlreadyThere(dv)) { + allMonths.add(dv); + } + } + + selectedMonth = allDates.first.month; + selectedYear = allDates.first.year; + datesInSelectedMonth = allDates.where((element) => element.month == selectedMonth).toList(); + } + + bool ifMonthAlreadyThere(DropValue monthDate) { + int index = allMonths.indexWhere((element) => element.id == monthDate.id && element.subValue == monthDate.subValue); + if (index == -1) { + return false; + } + + return true; + } + + bool ifDateAlreadyThere(DateTime dt) { + int index = allDates.indexWhere((element) => dt.month == element.month && dt.year == element.year); + if (index == -1) { + return false; + } + + return true; + } + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (BuildContext context, AppointmentsVM appointmentsVM, Widget? child) { + return Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Builder(builder: (context) { + return DropdownField( + (DropValue value) { + setState(() { + selectedYear = int.parse(value.value.split(',')[1]); + selectedMonth = value.value.split(',')[0].getMonthNumberByName(); + }); + }, + list: allMonths, + // dropdownValue: DropValue(1, "${selectedMonth.getMonthNameByNumber()}jhvk,", "${selectedYear}"), + hint: "${selectedMonth.getMonthNameByNumber()}, $selectedYear", + errorValue: "", + showAppointmentPickerVariant: true, + ); + }), + ), + Spacer(), + Icon( + Icons.calendar_today, + color: Colors.black, + size: 18, + ).paddingOnly(right: 10) + ], + ).paddingOnly(left: 6, right: 0), + TableCalendar( + headerVisible: false, + firstDay: datesInSelectedMonth.first, + lastDay: datesInSelectedMonth.last, + focusedDay: _focusedDay, + calendarFormat: _calendarFormat, + weekendDays: [DateTime.friday, DateTime.saturday], + daysOfWeekHeight: 30, + availableGestures: AvailableGestures.none, + daysOfWeekStyle: DaysOfWeekStyle( + weekdayStyle: TextStyle(fontSize: 14, color: MyColors.black), + weekendStyle: TextStyle(fontSize: 14, color: MyColors.black.withOpacity(0.5)), + ), + calendarBuilders: CalendarBuilders( + todayBuilder: (BuildContext context, DateTime dateTime1, DateTime dateTime2) { + return Container( + height: 50, + width: 50, + margin: EdgeInsets.all(5), + decoration: BoxDecoration( + color: Colors.blueAccent.withOpacity(0.4), + border: Border.all(color: Colors.orange, width: 2), + shape: BoxShape.circle, + ), + alignment: Alignment.center, + child: Text( + dateTime1.day.toString(), + ), + ); + }, + selectedBuilder: (BuildContext context, DateTime dateTime1, DateTime dateTime2) { + return Container( + height: 50, + width: 50, + margin: EdgeInsets.all(5), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: MyColors.darkIconColor, + ), + alignment: Alignment.center, + child: Text( + dateTime2.day.toString(), + style: TextStyle(color: MyColors.white), + ), + ); + }, + defaultBuilder: (BuildContext context, DateTime dateTime1, DateTime dateTime2) { + return Container( + height: 50, + width: 50, + margin: EdgeInsets.all(5), + decoration: BoxDecoration( + border: Border.all(color: Colors.orange, width: 2), + shape: BoxShape.circle, + ), + alignment: Alignment.center, + child: Text( + dateTime1.day.toString(), + ), + ); + }, + disabledBuilder: (BuildContext context, DateTime dateTime1, DateTime selectedDt) { + return Container( + height: 50, + width: 50, + margin: EdgeInsets.all(5), + decoration: BoxDecoration( + border: Border.all(color: Colors.grey), + shape: BoxShape.circle, + ), + alignment: Alignment.center, + child: Text( + dateTime1.day.toString(), + ), + ); + }, + ), + headerStyle: HeaderStyle(formatButtonVisible: false), + selectedDayPredicate: (day) { + // Use `selectedDayPredicate` to determine which day is currently selected. + // If this returns true, then `day` will be marked as selected. + + // Using `isSameDay` is recommended to disregard + // the time-part of compared DateTime objects. + return isSameDay(_selectedDay, day); + }, + onDaySelected: (selectedDay, focusedDay) { + if (!isSameDay(_selectedDay, selectedDay)) { + // Call `setState()` when updating the selected day + setState(() { + _selectedDay = selectedDay; + _focusedDay = selectedDay; + }); + } + }, + ), + ], + ); + }, + ); + } +} diff --git a/lib/views/provider/branch_detail_page.dart b/lib/views/provider/branch_detail_page.dart index e49cf16..0f26312 100644 --- a/lib/views/provider/branch_detail_page.dart +++ b/lib/views/provider/branch_detail_page.dart @@ -179,7 +179,7 @@ class _BranchDetailPageState extends State { showItem("Charges per KM", widget.branchDetailModel.branchServices![index].customerLocationRange.toString() + "SAR"), 8.height, ((widget.branchDetailModel.branchServices![index].itemsCount != null && widget.branchDetailModel.branchServices![index].itemsCount! > 0) - ? widget.branchDetailModel.branchServices![index].itemsCount.toString() + " items" + ? widget.branchDetailModel.branchServices![index].itemsCount.toString() + " items" : "No" + " items") .toText( fontSize: 12, diff --git a/lib/views/provider/sheet/items_list_sheet.dart b/lib/views/provider/sheet/items_list_sheet.dart index 0525c79..2ab4dc7 100644 --- a/lib/views/provider/sheet/items_list_sheet.dart +++ b/lib/views/provider/sheet/items_list_sheet.dart @@ -47,7 +47,7 @@ class _ItemsListSheetState extends State { crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start, children: [ - serviceItemModel.toString().toText(fontSize: 16, isBold: true), + serviceItemModel.name.toString().toText(fontSize: 16, isBold: true), 4.height, showItem("Available for appointment:", (serviceItemModel.isAllowAppointment ?? false) ? "Yes" : "No", valueColor: Colors.green), showItem("Allowing Workshop service:", (serviceItemModel.isAppointmentCompanyLoc ?? false) ? "Yes" : "No", valueColor: Colors.green), diff --git a/pubspec.yaml b/pubspec.yaml index a7d4453..6d94c35 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -34,6 +34,7 @@ dependencies: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.2 + table_calendar: ^3.0.9 mc_common_app: path: /Volumes/Data/Projects/Flutter/car_common_app From 2dd1952b208edebc0d453e6387a663b59f967d8c Mon Sep 17 00:00:00 2001 From: Faiz Hashmi Date: Thu, 26 Oct 2023 17:14:57 +0300 Subject: [PATCH 3/5] Completed the Appointment Flow --- lib/config/customer_dependencies.dart | 4 +- lib/main.dart | 4 +- ...hedule_repo.dart => appointment_repo.dart} | 64 +++-- lib/view_models/appointments_view_model.dart | 140 +++++++---- .../appointments/appointment_detail_view.dart | 196 +++++++++++---- .../book_appointment_schedules_view.dart | 30 +-- .../book_appointment_services_view.dart | 6 +- .../appointments/review_appointment_view.dart | 238 +++++++++--------- .../widgets/custom_calender_widget.dart | 84 ++++--- .../customer_appointment_slider_widget.dart | 74 +++--- lib/views/dashboard/dashboard_page.dart | 21 +- .../fragments/appointments_fragment.dart | 10 +- .../dashboard/fragments/home_fragment.dart | 9 +- lib/views/provider/branch_detail_page.dart | 18 +- 14 files changed, 533 insertions(+), 365 deletions(-) rename lib/repositories/{schedule_repo.dart => appointment_repo.dart} (68%) diff --git a/lib/config/customer_dependencies.dart b/lib/config/customer_dependencies.dart index 804a53f..7f5f1fb 100644 --- a/lib/config/customer_dependencies.dart +++ b/lib/config/customer_dependencies.dart @@ -1,12 +1,12 @@ import 'package:car_customer_app/repositories/provider_repo.dart'; -import 'package:car_customer_app/repositories/schedule_repo.dart'; +import 'package:car_customer_app/repositories/appointment_repo.dart'; import 'package:mc_common_app/config/dependencies.dart'; class CustomerDependencies { static void addDependencies() { AppDependencies.addDependencies(); injector.registerSingleton(() => ProviderRepoImp()); - injector.registerSingleton(() => ScheduleRepoImp()); + injector.registerSingleton(() => ScheduleRepoImp()); } } diff --git a/lib/main.dart b/lib/main.dart index bd7ca33..e526cf1 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,7 +1,7 @@ import 'package:car_customer_app/config/customer_dependencies.dart'; import 'package:car_customer_app/config/customer_routes.dart'; import 'package:car_customer_app/repositories/provider_repo.dart'; -import 'package:car_customer_app/repositories/schedule_repo.dart'; +import 'package:car_customer_app/repositories/appointment_repo.dart'; import 'package:car_customer_app/view_models/appointments_view_model.dart'; import 'package:car_customer_app/view_models/dashboard_view_model.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -56,7 +56,7 @@ Future main() async { ), ChangeNotifierProvider( create: (_) => AppointmentsVM( - scheduleRepo: injector.get(), + scheduleRepo: injector.get(), providerRepo: injector.get(), commonServices: injector.get(), commonRepo: injector.get(), diff --git a/lib/repositories/schedule_repo.dart b/lib/repositories/appointment_repo.dart similarity index 68% rename from lib/repositories/schedule_repo.dart rename to lib/repositories/appointment_repo.dart index 3ea0083..dc25367 100644 --- a/lib/repositories/schedule_repo.dart +++ b/lib/repositories/appointment_repo.dart @@ -1,3 +1,5 @@ +import 'dart:developer'; + import 'package:mc_common_app/api/api_client.dart'; import 'package:mc_common_app/classes/app_state.dart'; import 'package:mc_common_app/classes/consts.dart'; @@ -7,8 +9,9 @@ import 'package:mc_common_app/models/m_response.dart'; import 'package:mc_common_app/models/provider_branches_models/profile/services.dart'; import 'package:mc_common_app/models/schedule_model.dart'; import 'package:mc_common_app/models/service_schedule_model.dart'; +import 'package:mc_common_app/utils/enums.dart'; -abstract class ScheduleRepo { +abstract class AppointmentRepo { Future getAllServices(String branchId); Future createSchedule(Map map); @@ -26,10 +29,12 @@ abstract class ScheduleRepo { required List serviceItemIdsForWorkshop, }); - Future createServiceAppointment({required List serviceItemIds, required int serviceSlotID}); + Future createServiceAppointment({required List schedules, required int serviceProviderID}); + + Future cancelOrRescheduleServiceAppointment({required int serviceAppointmentID, required int serviceSlotID, required int appointmentScheduleAction}); } -class ScheduleRepoImp implements ScheduleRepo { +class ScheduleRepoImp implements AppointmentRepo { @override Future getAllServices(String branchId) async { Map map = {"ProviderBranchID": branchId}; @@ -98,26 +103,57 @@ class ScheduleRepoImp implements ScheduleRepo { token: t, ); List serviceAppointmentScheduleModel = - List.generate(adsGenericModel.data.length, (index) => ServiceAppointmentScheduleModel.fromJson(adsGenericModel.data[index])); + List.generate(adsGenericModel.data.length, (index) => ServiceAppointmentScheduleModel.fromJson(adsGenericModel.data[index], isForAppointment: true)); return serviceAppointmentScheduleModel; } - Future createServiceAppointment({required List serviceItemIds, required int serviceSlotID}) async { + Future createServiceAppointment({required List schedules, required int serviceProviderID}) async { + String t = AppState().getUser.data!.accessToken ?? ""; + int customerId = AppState().getUser.data!.userInfo!.customerId ?? 0; + + List> mapList = []; + schedules.forEach((schedule) { + List serviceItemIds = []; + schedule.servicesListInAppointment!.forEach((service) { + service.serviceItems!.forEach((item) { + serviceItemIds.add(item.id!); + }); + }); + mapList.add({ + "serviceSlotID": schedule.selectedCustomTimeDateSlotModel!.date!.slotId, + "serviceProviderID": serviceProviderID, + "customerID": customerId, + "serviceItemID": serviceItemIds, + }); + }); + + GenericRespModel adsGenericModel = await injector.get().postJsonForObject( + (json) => GenericRespModel.fromJson(json), + ApiConsts.ServiceProvidersAppointmentCreate, + mapList, + token: t, + ); + + return adsGenericModel; + } + + @override + Future cancelOrRescheduleServiceAppointment({required int serviceAppointmentID, required int serviceSlotID, required int appointmentScheduleAction}) async { String t = AppState().getUser.data!.accessToken ?? ""; - var queryParameters = { - "id": 0, + + final payload = { + "serviceAppointmentID": serviceAppointmentID, "serviceSlotID": serviceSlotID, - "appointmentStatusID": 0, - "serviceProviderID": 0, - "customerID": 0, - "isActive": true, - "serviceItemID": [11] + "appointmentScheduleAction": appointmentScheduleAction, }; + GenericRespModel adsGenericModel = await injector.get().postJsonForObject( (json) => GenericRespModel.fromJson(json), - ApiConsts.ServiceProvidersAppointmentCreate, - queryParameters, + ApiConsts.ServiceProviderAppointmentRescheduleCancelAppointment, + payload, token: t, ); + + return adsGenericModel; } } diff --git a/lib/view_models/appointments_view_model.dart b/lib/view_models/appointments_view_model.dart index e5a4d38..d87f3be 100644 --- a/lib/view_models/appointments_view_model.dart +++ b/lib/view_models/appointments_view_model.dart @@ -1,15 +1,14 @@ -import 'dart:developer'; - import 'package:car_customer_app/repositories/provider_repo.dart'; -import 'package:car_customer_app/repositories/schedule_repo.dart'; +import 'package:car_customer_app/repositories/appointment_repo.dart'; +import 'package:car_customer_app/view_models/dashboard_view_model.dart'; import 'package:car_customer_app/views/appointments/widgets/appointment_service_pick_bottom_sheet.dart'; -import 'package:flutter/cupertino.dart'; 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/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'; @@ -23,21 +22,25 @@ 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/payment_view_model.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 ScheduleRepo scheduleRepo; + 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 = []; @@ -50,22 +53,6 @@ class AppointmentsVM extends BaseVM { List serviceAppointmentScheduleList = []; - // Future getSchedulesByBranchId() async { - // availableSchedules = await scheduleRepo.getSchedules(selectedBranchModel!.id!.toString()); - // log("schedules: ${availableSchedules.toString()}"); - // notifyListeners(); - // } - - List servicesInSchedule = []; - - bool ifServiceAlreadyThere(int id) { - int index = servicesInSchedule.indexWhere((element) => element.id == id); - if (index == -1) { - return false; - } - return true; - } - bool ifItemAlreadySelected(int id) { int indexFound = allSelectedItemsInAppointments.indexWhere((element) => element.id == id); if (indexFound != -1) { @@ -96,14 +83,73 @@ class AppointmentsVM extends BaseVM { Future onBookAppointmentPressed(BuildContext context) async { Utils.showLoading(context); + bool isSuccess = false; + List appointmentIdsList = []; try { - serviceAppointmentScheduleList.forEach((schedule) async { - List serviceItemIds = []; - // schedule.serviceItemList!.forEach((serviceItem) { - // serviceItemIds.add(serviceItem.id.toString()); - // }); - await scheduleRepo.createServiceAppointment(serviceItemIds: serviceItemIds, serviceSlotID: schedule.selectedCustomTimeDateSlotModel!.date!.slotId); - }); + 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 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()}"); } @@ -160,6 +206,11 @@ class AppointmentsVM extends BaseVM { currentServiceSelection = null; } + resetAfterBookingAppointment() { + servicesInCurrentAppointment.clear(); + allSelectedItemsInAppointments.clear(); + } + populateAppointmentsFilterList() { appointmentsFilterOptions.clear(); appointmentsFilterOptions = [ @@ -172,18 +223,29 @@ class AppointmentsVM extends BaseVM { notifyListeners(); } - applyFilterOnAppointmentsVM({required int index}) { + applyFilterOnAppointmentsVM({required AppointmentStatusEnum appointmentStatusEnum}) { if (appointmentsFilterOptions.isEmpty) return; for (var value in appointmentsFilterOptions) { value.isSelected = false; } - appointmentsFilterOptions[index].isSelected = true; + appointmentsFilterOptions[appointmentStatusEnum.getIdFromAppointmentStatusEnum() == -1 ? 0 : appointmentStatusEnum.getIdFromAppointmentStatusEnum()].isSelected = true; + + if (appointmentStatusEnum.getIdFromAppointmentStatusEnum() == -1) { + myFilteredAppointments = myAppointments; + notifyListeners(); + return; + } + + myFilteredAppointments = myAppointments.where((element) => element.appointmentStatusID! == appointmentStatusEnum.getIdFromAppointmentStatusEnum()).toList(); notifyListeners(); } Future getMyAppointments() async { isFetchingLists = true; myAppointments = await commonRepo.getMyAppointments(); + myFilteredAppointments = myAppointments; + myUpComingAppointments = myAppointments.where((element) => element.appointmentStatusEnum == AppointmentStatusEnum.booked).toList(); + // applyFilterOnAppointmentsVM(appointmentStatusEnum: AppointmentStatusEnum.allAppointments); isFetchingLists = false; notifyListeners(); } @@ -259,7 +321,7 @@ class AppointmentsVM extends BaseVM { notifyListeners(); } - populateProvidersFilterList() { + populateBranchesFilterList() { providersFilterOptions.clear(); providersFilterOptions = [ FilterListModel(title: "All Providers", isSelected: true, id: -1), @@ -455,7 +517,7 @@ class AppointmentsVM extends BaseVM { crossAxisAlignment: CrossAxisAlignment.end, children: [ (selectedService.isHomeSelected - ? "${(selectedService.currentTotalServicePrice ?? 0.0) + (double.parse((selectedService.rangePricePerKm ?? "0.0")) * totalKms)}" + ? "${(selectedService.currentTotalServicePrice) + (double.parse((selectedService.rangePricePerKm ?? "0.0")) * totalKms)}" : "${selectedService.currentTotalServicePrice}") .toText(fontSize: 29, isBold: true), 2.width, @@ -515,10 +577,6 @@ class AppointmentsVM extends BaseVM { } }); - servicesInSchedule.clear(); - - //TODO: WE HAVE TO ADD PARAMETER IN THE allSelectedItemsInAppointments TO DECIDE WHETHER THE APPOINTMENT IS FOR HOME OR WORKSHOP - serviceAppointmentScheduleList = await scheduleRepo.mergeServiceIntoAvailableSchedules( serviceItemIdsForHome: serviceItemIdsForHome, serviceItemIdsForWorkshop: serviceItemIdsForWorkshop, @@ -529,16 +587,12 @@ class AppointmentsVM extends BaseVM { 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); - - schedule.servicesListInAppointment!.forEach((service) { - if (!ifServiceAlreadyThere(service.serviceProviderServiceId!)) { - servicesInSchedule.add(DropValue(service.serviceProviderServiceId!, service.providerServiceDescription!, "")); - } - }); + totalAmount = totalAmount + (schedule.amountTotal ?? 0.0); }, ); Utils.hideLoading(context); diff --git a/lib/views/appointments/appointment_detail_view.dart b/lib/views/appointments/appointment_detail_view.dart index 718d69d..5b8d01d 100644 --- a/lib/views/appointments/appointment_detail_view.dart +++ b/lib/views/appointments/appointment_detail_view.dart @@ -1,13 +1,18 @@ +import 'package:car_customer_app/view_models/appointments_view_model.dart'; import 'package:flutter/material.dart'; import 'package:mc_common_app/classes/consts.dart'; import 'package:mc_common_app/extensions/int_extensions.dart'; import 'package:mc_common_app/extensions/string_extensions.dart'; import 'package:mc_common_app/models/appointments_models/appointment_list_model.dart'; +import 'package:mc_common_app/models/services/service_model.dart'; import 'package:mc_common_app/theme/colors.dart'; +import 'package:mc_common_app/utils/dialogs_and_bottomsheets.dart'; +import 'package:mc_common_app/utils/enums.dart'; import 'package:mc_common_app/widgets/button/show_fill_button.dart'; import 'package:mc_common_app/widgets/common_widgets/app_bar.dart'; import 'package:mc_common_app/widgets/common_widgets/card_button_with_icon.dart'; import 'package:mc_common_app/widgets/extensions/extensions_widget.dart'; +import 'package:provider/provider.dart'; class AppointmentDetailView extends StatelessWidget { final AppointmentListModel appointmentListModel; @@ -21,6 +26,97 @@ class AppointmentDetailView extends StatelessWidget { "Oil Change", ]; + Widget getBaseActionButtonWidget({required Color color, required String text, Color textColor = MyColors.white, required Function() onPressed}) { + return Expanded( + child: ShowFillButton( + maxHeight: 55, + title: text, + onPressed: onPressed, + backgroundColor: color, + txtColor: textColor, + fontSize: 18, + ), + ); + } + + Widget buildBottomActionButton({required AppointmentStatusEnum appointmentStatusEnum, required BuildContext context}) { + switch (appointmentStatusEnum) { + case AppointmentStatusEnum.booked: + return Align( + alignment: Alignment.bottomCenter, + child: Row( + children: [ + getBaseActionButtonWidget(color: MyColors.redColor, onPressed: () => appointmentCancelConfirmationSheet(context), text: "Cancel"), + 12.width, + getBaseActionButtonWidget(color: MyColors.greenColor, onPressed: () {}, text: "Confirm"), + ], + ), + ); + case AppointmentStatusEnum.confirmed: + return Align( + alignment: Alignment.bottomCenter, + child: Row( + children: [ + getBaseActionButtonWidget(color: MyColors.redColor, onPressed: () => appointmentCancelConfirmationSheet(context), text: "Cancel"), + ], + ), + ); + case AppointmentStatusEnum.arrived: + return Align( + alignment: Alignment.bottomCenter, + child: Row( + children: [ + getBaseActionButtonWidget(color: MyColors.grey98Color.withOpacity(0.3), textColor: MyColors.lightTextColor, onPressed: () {}, text: "In Progress"), + ], + ), + ); + case AppointmentStatusEnum.cancelled: + return Align( + alignment: Alignment.bottomCenter, + child: Row( + children: [ + getBaseActionButtonWidget(color: MyColors.grey98Color.withOpacity(0.3), textColor: MyColors.lightTextColor, onPressed: () {}, text: "Cancelled"), + ], + ), + ); + case AppointmentStatusEnum.allAppointments: + return SizedBox(); + } + } + + void appointmentCancelConfirmationSheet(BuildContext context) { + final appointmentsVm = context.read(); + return actionConfirmationBottomSheet( + context: context, + title: "Do you want to cancel this appointment?".toText(fontSize: 28, isBold: true, letterSpacing: -1.44), + subtitle: "Your appointment will be cancelled and you cannot undo this action.", + actionButtonYes: Expanded( + child: ShowFillButton( + maxHeight: 55, + title: "Yes", + fontSize: 15, + onPressed: () { + Navigator.pop(context); + appointmentsVm.onCancelAppointmentPressed(context: context, appointmentListModel: appointmentListModel); + }, + ), + ), + actionButtonNo: Expanded( + child: ShowFillButton( + maxHeight: 55, + isFilled: false, + borderColor: MyColors.darkPrimaryColor, + title: "No", + txtColor: MyColors.darkPrimaryColor, + fontSize: 15, + onPressed: () { + Navigator.pop(context); + }, + ), + ), + ); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -52,29 +148,42 @@ class AppointmentDetailView extends StatelessWidget { ], ), 13.height, - Row( - children: [ - MyAssets.maintenanceIcon.buildSvg( - height: 10, - width: 10, - fit: BoxFit.fill, - ), - 10.width, - "Maintenance".toText(fontSize: 18, isBold: true), - ], - ), - Column( - children: servicesList - .map((e) => e - .toText( - textAlign: TextAlign.start, - fontSize: 13, - isBold: true, - color: MyColors.lightTextColor, - ) - .paddingOnly(bottom: 5)) - .toList(), - ).paddingOnly(left: 15), + if (appointmentListModel.appointmentServicesList != null && appointmentListModel.appointmentServicesList!.isNotEmpty) ...[ + Column( + children: List.generate(appointmentListModel.appointmentServicesList!.length, (index) { + ServiceModel service = appointmentListModel.appointmentServicesList![index]; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + // MyAssets.maintenanceIcon.buildSvg( + // height: 10, + // width: 10, + // fit: BoxFit.fill, + // ), + // 10.width, + "${index + 1}. ${service.providerServiceDescription}".toText(fontSize: 18, isBold: true), + ], + ), + if (service.serviceItems != null && service.serviceItems!.isNotEmpty) ...[ + Column( + children: List.generate( + service.serviceItems!.length, + (index) => "${service.serviceItems![index].name}".toText( + textAlign: TextAlign.start, + fontSize: 13, + isBold: true, + color: MyColors.lightTextColor, + ), + ), + ).paddingOnly(left: 15), + ], + ], + ); + }), + ), + ], 15.height, Row( children: [ @@ -83,43 +192,20 @@ class AppointmentDetailView extends StatelessWidget { onCardTapped: () {}, icon: MyAssets.scheduleAppointmentIcon.buildSvg(), ), - 10.width, - CardButtonWithIcon( - title: "Pay for Appointment", - onCardTapped: () {}, - icon: MyAssets.creditCardIcon.buildSvg(), - ), + if (appointmentListModel.appointmentStatusEnum == AppointmentStatusEnum.booked) ...[ + 10.width, + CardButtonWithIcon( + title: "Pay for Appointment", + onCardTapped: () {}, + icon: MyAssets.creditCardIcon.buildSvg(), + ), + ], ], ), 15.height, ], ).toWhiteContainer(width: double.infinity, allPading: 12), - Align( - alignment: Alignment.bottomCenter, - child: Row( - children: [ - Expanded( - child: ShowFillButton( - maxHeight: 55, - title: "Cancel", - onPressed: () {}, - backgroundColor: MyColors.redColor, - ), - ), - if (appointmentListModel.appointmentStatusID == 1) ...[ - 12.width, - Expanded( - child: ShowFillButton( - maxHeight: 55, - title: "Confirm", - onPressed: () {}, - backgroundColor: MyColors.greenColor, - ), - ), - ], - ], - ), - ), + buildBottomActionButton(appointmentStatusEnum: appointmentListModel.appointmentStatusEnum!, context: context), ], ), ), diff --git a/lib/views/appointments/book_appointment_schedules_view.dart b/lib/views/appointments/book_appointment_schedules_view.dart index 8b37974..d6614d7 100644 --- a/lib/views/appointments/book_appointment_schedules_view.dart +++ b/lib/views/appointments/book_appointment_schedules_view.dart @@ -1,22 +1,15 @@ import 'package:car_customer_app/view_models/appointments_view_model.dart'; -import 'package:car_customer_app/views/appointments/widgets/appointment_service_pick_bottom_sheet.dart'; import 'package:car_customer_app/views/appointments/widgets/custom_calender_widget.dart'; 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/service_schedule_model.dart'; -import 'package:mc_common_app/models/services/item_model.dart'; -import 'package:mc_common_app/models/widgets_models.dart'; +import 'package:mc_common_app/models/services/service_model.dart'; import 'package:mc_common_app/theme/colors.dart'; -import 'package:mc_common_app/utils/navigator.dart'; -import 'package:mc_common_app/views/advertisement/custom_add_button.dart'; import 'package:mc_common_app/widgets/button/show_fill_button.dart'; import 'package:mc_common_app/widgets/common_widgets/app_bar.dart'; -import 'package:mc_common_app/widgets/common_widgets/info_bottom_sheet.dart'; import 'package:mc_common_app/widgets/common_widgets/time_slots.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'; @@ -70,13 +63,13 @@ class BookAppointmentSchedulesView extends StatelessWidget { ListView.builder( physics: NeverScrollableScrollPhysics(), shrinkWrap: true, - itemCount: appointmentsVM.servicesInSchedule.length, + itemCount: appointmentsVM.serviceAppointmentScheduleList[scheduleIndex].servicesListInAppointment!.length, itemBuilder: (BuildContext context, int serviceIndex) { - DropValue selectedService = appointmentsVM.servicesInSchedule[serviceIndex]; + ServiceModel selectedService = appointmentsVM.serviceAppointmentScheduleList[scheduleIndex].servicesListInAppointment![serviceIndex]; return Row( children: [ Expanded( - child: ("${serviceIndex + 1}. ${selectedService.value}").toText(fontSize: 15, isBold: true, color: MyColors.lightTextColor), + child: ("${serviceIndex + 1}. ${selectedService.providerServiceDescription}").toText(fontSize: 15, isBold: true, color: MyColors.lightTextColor), ), ], ); @@ -89,18 +82,7 @@ class BookAppointmentSchedulesView extends StatelessWidget { children: [ Column( children: [ - if (true) ...[ - // SizedBox( - // width: double.infinity, - // child: BuildDateSlotsForAppointment( - // customDateSlots: scheduleData.customTimeDateSlotList ?? [], - // onPressed: (dateIndex) { - // appointmentsVM.updateSelectedAppointmentDate(scheduleIndex: scheduleIndex, dateIndex: dateIndex); - // }, - // ), - // ), - CustomCalenderWidget(customTimeDateSlotList: scheduleData.customTimeDateSlotList ?? []), - ], + CustomCalenderWidget(customTimeDateSlotList: scheduleData.customTimeDateSlotList ?? [], scheduleIndex: scheduleIndex), if (appointmentsVM.serviceAppointmentScheduleList[scheduleIndex].selectedDateIndex != null) ...[ 5.height, Row( @@ -135,7 +117,7 @@ class BookAppointmentSchedulesView extends StatelessWidget { txtColor: MyColors.black, maxHeight: 55, title: "Cancel", - onPressed: () {}, + onPressed: () => Navigator.pop(context), backgroundColor: MyColors.greyButtonColor, ), ), diff --git a/lib/views/appointments/book_appointment_services_view.dart b/lib/views/appointments/book_appointment_services_view.dart index cad86a8..c149114 100644 --- a/lib/views/appointments/book_appointment_services_view.dart +++ b/lib/views/appointments/book_appointment_services_view.dart @@ -104,7 +104,11 @@ class BookAppointmentServicesView extends StatelessWidget { txtColor: MyColors.black, maxHeight: 55, title: "Cancel", - onPressed: () {}, + onPressed: () { + appointmentsVM.servicesInCurrentAppointment.clear(); + appointmentsVM.allSelectedItemsInAppointments.clear(); + Navigator.pop(context); + }, backgroundColor: MyColors.greyButtonColor, ), ), diff --git a/lib/views/appointments/review_appointment_view.dart b/lib/views/appointments/review_appointment_view.dart index 5862e51..ff4d5ae 100644 --- a/lib/views/appointments/review_appointment_view.dart +++ b/lib/views/appointments/review_appointment_view.dart @@ -1,3 +1,5 @@ +import 'dart:developer'; + import 'package:car_customer_app/view_models/appointments_view_model.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; @@ -5,6 +7,7 @@ import 'package:mc_common_app/classes/consts.dart'; import 'package:mc_common_app/extensions/int_extensions.dart'; import 'package:mc_common_app/extensions/string_extensions.dart'; import 'package:mc_common_app/generated/locale_keys.g.dart'; +import 'package:mc_common_app/models/schedule_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'; @@ -93,7 +96,7 @@ class ReviewAppointment extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( - child: "Schedule: ${scheduleIndex + 1}".toText(fontSize: 20, isBold: true), + child: "Services".toText(fontSize: 14, isBold: true, color: MyColors.lightTextColor), ), ], ), @@ -103,7 +106,7 @@ class ReviewAppointment extends StatelessWidget { ListView.separated( physics: NeverScrollableScrollPhysics(), shrinkWrap: true, - itemCount: appointmentsVM.servicesInSchedule.length, + itemCount: scheduleData.servicesListInAppointment!.length, itemBuilder: (BuildContext context, int serviceIndex) { String selectedTimeSlot = ""; if (scheduleData.selectedCustomTimeDateSlotModel!.availableSlots != null) { @@ -111,33 +114,38 @@ class ReviewAppointment extends StatelessWidget { } return Column( children: [ - // Row( - // children: [ - // Expanded( - // child: selectedService.value.toText(fontSize: 14, isBold: true), - // ), - // ], - // ), - if (true) ...[ - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - "Service Location: ".toText(fontSize: 12, color: MyColors.lightTextColor, isBold: true), - (appointmentsVM.isHomeTapped ? "Home" : "Workshop").toText(fontSize: 12, isBold: true).expand(), - ], - ), - 5.height, + if (scheduleData.servicesListInAppointment!.isNotEmpty) ...[ Column( - children: List.generate(scheduleData.servicesListInAppointment!.length, (itemIndex) { - ServiceModel serviceData = scheduleData.servicesListInAppointment![itemIndex]; - return Row( - crossAxisAlignment: CrossAxisAlignment.start, + children: List.generate(scheduleData.servicesListInAppointment!.length, (serviceIndex) { + ServiceModel serviceData = scheduleData.servicesListInAppointment![serviceIndex]; + return Column( children: [ - "${serviceData.providerServiceDescription}".toText(fontSize: 13, color: MyColors.lightTextColor, isBold: true), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "${serviceData.providerServiceDescription}".toText(fontSize: 16, isBold: true), + ], + ), + Column( + children: List.generate(serviceData.serviceItems!.length, (itemIndex) { + ItemData itemData = serviceData.serviceItems![itemIndex]; + return Column( + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + "${itemData.name}: ${itemData.price} SAR".toText(fontSize: 13, isBold: true, color: MyColors.lightTextColor), + ], + ), + ], + ); + }), + ), ], ); }), - ), + ).paddingOnly(bottom: 10), ], 5.height, SizedBox( @@ -145,7 +153,7 @@ class ReviewAppointment extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - "Time & Location".toText(fontSize: 14, isBold: true), + "Time & Location".toText(fontSize: 16, isBold: true), 3.height, Row(children: [ "Date & Time: ".toText(fontSize: 12, color: MyColors.lightTextColor, isBold: true), @@ -153,11 +161,43 @@ class ReviewAppointment extends StatelessWidget { ]), Row(children: [ "Location: ".toText(fontSize: 12, color: MyColors.lightTextColor, isBold: true), - "PM58+F97, Al Olaya, Riyadh 12333".toText(fontSize: 12, isBold: true), + (scheduleData.appointmentType == 2 ? "Home" : "Workshop").toText(fontSize: 12, isBold: true), ]), ], ), - ) + ), + 10.height, + Divider(thickness: 0.7), + Builder(builder: (BuildContext context) { + double totalServicePrice = 0.0; + double totalKms = 15.3; + double rangePricePerKm = 5; + totalServicePrice = rangePricePerKm * totalKms; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + "Amount".toText(fontSize: 16, isBold: true), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + "Service Charges".toText(fontSize: 14, color: MyColors.lightTextColor, isBold: true), + "${scheduleData.amountTotal.toString()} SAR".toText(fontSize: 16, isBold: true), + ], + ), + if (scheduleData.appointmentType == 1) ...[ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + "Location Charges ($rangePricePerKm x $totalKms )".toText(fontSize: 14, color: MyColors.lightTextColor, isBold: true), + "${totalServicePrice.toString()} SAR".toText(fontSize: 16, isBold: true), + ], + ), + ], + 10.height, + ], + ); + }), ], ); }, @@ -171,111 +211,60 @@ class ReviewAppointment extends StatelessWidget { ); } - Widget buildTimeAndLocationInfoCard() { - return SizedBox( - width: double.infinity, + Widget buildNextButtonFooter({required BuildContext context}) { + AppointmentsVM appointmentsVM = context.read(); + return Container( + color: MyColors.white, child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - "Time & Location".toText(fontSize: 16, isBold: true), - 3.height, - Row(children: [ - "Date & Time: ".toText(fontSize: 12, color: MyColors.lightTextColor, isBold: true), - "2nd Feb, 2023 at 09:30 AM".toText(fontSize: 12, isBold: true), - ]), - Row(children: [ - "Location: ".toText(fontSize: 12, color: MyColors.lightTextColor, isBold: true), - "PM58+F97, Al Olaya, Riyadh 12333".toText(fontSize: 12, isBold: true), - ]), - ], - ).toWhiteContainer(width: double.infinity, allPading: 12, margin: EdgeInsets.only(left: 21, right: 21, top: 10)), - ); - } - - Widget buildChargesBreakDown({required BuildContext context}) { - AppointmentsVM appointmentsVM = context.read(); - List allSelectedItems = []; - double totalServicePrice = 0.0; - // appointmentsVM.serviceAppointmentScheduleList.forEach((schedule) { - // schedule.servicesListInAppointment!.forEach((service) { - // allSelectedItems.add(item); - // totalServicePrice = totalServicePrice + double.parse(item.price!); - // }); - // }); - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - "Services".toText(fontSize: 16, isBold: true), - Column( - children: List.generate( - allSelectedItems.length, - (index) => Row( + Divider(thickness: 0.7, height: 3), + 8.height, + if (appointmentsVM.amountToPayForAppointment > 0) ...[ + Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - "${allSelectedItems[index].name}".toText(fontSize: 12, color: MyColors.lightTextColor, isBold: true), - "${allSelectedItems[index].price} SAR".toText(fontSize: 12, isBold: true), + "Payable now".toText(fontSize: 14, isBold: true), + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + appointmentsVM.amountToPayForAppointment.toString().toText(fontSize: 16, isBold: true), + 2.width, + "SAR".toText(color: MyColors.lightTextColor, fontSize: 12, isBold: true).paddingOnly(bottom: 2), + ], + ) ], - ), - ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - "${totalServicePrice.toString()} SAR".toText(fontSize: 16, isBold: true), + ).paddingOnly(left: 21, right: 21), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + "Remaining Amount".toText(fontSize: 14, isBold: true), + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + (appointmentsVM.totalAmount - appointmentsVM.amountToPayForAppointment).toString().toText(fontSize: 16, isBold: true), + 2.width, + "SAR".toText(color: MyColors.lightTextColor, fontSize: 12, isBold: true).paddingOnly(bottom: 2), + ], + ) + ], + ).paddingOnly(left: 21, right: 21), ], - ), - 10.height, - Divider(thickness: 0.7), - if (appointmentsVM.isHomeTapped) ...[ - "Home Location".toText(fontSize: 16, isBold: true), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - "10km ".toText(fontSize: 12, color: MyColors.lightTextColor, isBold: true), - "5 x 10".toText(fontSize: 12, isBold: true), - ], - ), - 8.height, - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - "50 SAR".toText(fontSize: 16, isBold: true), + "Total Amount ".toText(fontSize: 18, isBold: true), + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + appointmentsVM.totalAmount.toString().toText(fontSize: 29, isBold: true), + 2.width, + "SAR".toText(color: MyColors.lightTextColor, fontSize: 16, isBold: true).paddingOnly(bottom: 5), + ], + ) ], - ), - 10.height, - Divider(thickness: 0.7), - ], - 10.height, - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - "Total Amount ".toText(fontSize: 16, isBold: true), - Row( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - totalServicePrice.toString().toText(fontSize: 29, isBold: true), - 2.width, - "SAR".toText(color: MyColors.lightTextColor, fontSize: 16, isBold: true).paddingOnly(bottom: 5), - ], - ) - ], - ), - 10.height, - ], - ).toWhiteContainer(width: double.infinity, allPading: 12, margin: EdgeInsets.only(left: 21, right: 21, top: 10, bottom: 21)); - } - - Widget buildNextButtonFooter({required BuildContext context}) { - AppointmentsVM appointmentsVM = context.read(); - return Container( - height: 90, - color: MyColors.white, - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Divider(thickness: 0.7, height: 3), - 10.height, + ).paddingOnly(left: 21, right: 21), + 5.height, SizedBox( width: double.infinity, child: ShowFillButton( @@ -283,7 +272,7 @@ class ReviewAppointment extends StatelessWidget { backgroundColor: MyColors.darkPrimaryColor, title: "Book Appointment", onPressed: () { - // appointmentsVM.onBookAppointmentPressed(); + appointmentsVM.onBookAppointmentPressed(context); }, ).paddingOnly(bottom: 12, left: 21, right: 21), ), @@ -299,7 +288,6 @@ class ReviewAppointment extends StatelessWidget { title: "Review Appointment", isRemoveBackButton: false, isDrawerEnabled: false, - actions: [MyAssets.searchIcon.buildSvg().paddingOnly(right: 21)], onBackButtonTapped: () => Navigator.pop(context), ), body: Column( @@ -309,8 +297,6 @@ class ReviewAppointment extends StatelessWidget { children: [ buildBranchInfoCard(context: context), buildServicesInfoCard(context: context), - // buildTimeAndLocationInfoCard(), - buildChargesBreakDown(context: context), ], ).expand(), buildNextButtonFooter(context: context), diff --git a/lib/views/appointments/widgets/custom_calender_widget.dart b/lib/views/appointments/widgets/custom_calender_widget.dart index 7b86c0d..de5c9b5 100644 --- a/lib/views/appointments/widgets/custom_calender_widget.dart +++ b/lib/views/appointments/widgets/custom_calender_widget.dart @@ -2,7 +2,6 @@ import 'dart:developer'; import 'package:car_customer_app/view_models/appointments_view_model.dart'; import 'package:flutter/material.dart'; -import 'package:mc_common_app/extensions/int_extensions.dart'; import 'package:mc_common_app/extensions/string_extensions.dart'; import 'package:mc_common_app/models/service_schedule_model.dart'; import 'package:mc_common_app/theme/colors.dart'; @@ -13,9 +12,10 @@ import 'package:provider/provider.dart'; import 'package:intl/intl.dart'; class CustomCalenderWidget extends StatefulWidget { - final List? customTimeDateSlotList; + final List customTimeDateSlotList; + final int scheduleIndex; - const CustomCalenderWidget({super.key, required this.customTimeDateSlotList}); + const CustomCalenderWidget({super.key, required this.customTimeDateSlotList, required this.scheduleIndex}); @override State createState() => _CustomCalenderWidgetState(); @@ -41,22 +41,30 @@ class _CustomCalenderWidgetState extends State { } populateDateList() { - for (var value in widget.customTimeDateSlotList!) { + for (var value in widget.customTimeDateSlotList) { DateTime dt = DateFormat('dd MMMM, yyyy').parse(value.date!.date); - // TODO: THIS COMPARISON NEEDS TO BE FIXED!!!!! - // TODO: BECAUSE WE CAN ONLY VALUE IN DROPDOWN, SO SUBVALUE IS OF NO USE!! - - DropValue dv = DropValue(dt.month, "${dt.month.getMonthNameByNumber()}, ${dt.year}", ""); + DropValue dv = DropValue(dt.month, "${dt.month.getMonthNameByNumber()}, ${dt.year}", "${dt.year}"); allDates.add(dt); if (!ifMonthAlreadyThere(dv)) { allMonths.add(dv); } } - selectedMonth = allDates.first.month; selectedYear = allDates.first.year; + final appointmentsVM = context.read(); + if (appointmentsVM.serviceAppointmentScheduleList[widget.scheduleIndex].selectedCustomTimeDateSlotModel != null) { + DateTime alreadySelectedDate = DateFormat('dd MMMM, yyyy').parse(appointmentsVM.serviceAppointmentScheduleList[widget.scheduleIndex].selectedCustomTimeDateSlotModel!.date!.date); + _selectedDay = alreadySelectedDate; + _focusedDay = alreadySelectedDate; + selectedMonth = alreadySelectedDate.month; + selectedYear = alreadySelectedDate.year; + } datesInSelectedMonth = allDates.where((element) => element.month == selectedMonth).toList(); + + if (appointmentsVM.serviceAppointmentScheduleList[widget.scheduleIndex].selectedCustomTimeDateSlotModel == null) { + _focusedDay = datesInSelectedMonth.first; + } } bool ifMonthAlreadyThere(DropValue monthDate) { @@ -64,7 +72,6 @@ class _CustomCalenderWidgetState extends State { if (index == -1) { return false; } - return true; } @@ -73,7 +80,6 @@ class _CustomCalenderWidgetState extends State { if (index == -1) { return false; } - return true; } @@ -93,10 +99,12 @@ class _CustomCalenderWidgetState extends State { setState(() { selectedYear = int.parse(value.value.split(',')[1]); selectedMonth = value.value.split(',')[0].getMonthNumberByName(); + datesInSelectedMonth = allDates.where((element) => element.month == selectedMonth).toList(); + _focusedDay = datesInSelectedMonth.first; }); }, list: allMonths, - // dropdownValue: DropValue(1, "${selectedMonth.getMonthNameByNumber()}jhvk,", "${selectedYear}"), + dropdownValue: DropValue(selectedMonth, "${selectedMonth.getMonthNameByNumber()}, ${selectedYear}", "${selectedYear}"), hint: "${selectedMonth.getMonthNameByNumber()}, $selectedYear", errorValue: "", showAppointmentPickerVariant: true, @@ -125,22 +133,6 @@ class _CustomCalenderWidgetState extends State { weekendStyle: TextStyle(fontSize: 14, color: MyColors.black.withOpacity(0.5)), ), calendarBuilders: CalendarBuilders( - todayBuilder: (BuildContext context, DateTime dateTime1, DateTime dateTime2) { - return Container( - height: 50, - width: 50, - margin: EdgeInsets.all(5), - decoration: BoxDecoration( - color: Colors.blueAccent.withOpacity(0.4), - border: Border.all(color: Colors.orange, width: 2), - shape: BoxShape.circle, - ), - alignment: Alignment.center, - child: Text( - dateTime1.day.toString(), - ), - ); - }, selectedBuilder: (BuildContext context, DateTime dateTime1, DateTime dateTime2) { return Container( height: 50, @@ -158,6 +150,22 @@ class _CustomCalenderWidgetState extends State { ); }, defaultBuilder: (BuildContext context, DateTime dateTime1, DateTime dateTime2) { + int index = datesInSelectedMonth.indexWhere((element) => isSameDay(dateTime1, element)); + if (index == -1) { + return Container( + height: 50, + width: 50, + margin: EdgeInsets.all(5), + decoration: BoxDecoration( + border: Border.all(color: Colors.grey), + shape: BoxShape.circle, + ), + alignment: Alignment.center, + child: Text( + dateTime1.day.toString(), + ), + ); + } return Container( height: 50, width: 50, @@ -198,12 +206,28 @@ class _CustomCalenderWidgetState extends State { return isSameDay(_selectedDay, day); }, onDaySelected: (selectedDay, focusedDay) { + int index = datesInSelectedMonth.indexWhere((element) => isSameDay(selectedDay, element)); + + if (index == -1) { + return; + } if (!isSameDay(_selectedDay, selectedDay)) { - // Call `setState()` when updating the selected day setState(() { _selectedDay = selectedDay; - _focusedDay = selectedDay; + _focusedDay = focusedDay; }); + + int dateIndex = widget.customTimeDateSlotList.indexWhere((element) { + DateTime dateFromList = DateFormat('dd MMMM, yyyy').parse(element.date!.date); + if (isSameDay(dateFromList, selectedDay)) { + return true; + } + return false; + }); + + if (dateIndex != -1) { + appointmentsVM.updateSelectedAppointmentDate(scheduleIndex: widget.scheduleIndex, dateIndex: dateIndex); + } } }, ), diff --git a/lib/views/appointments/widgets/customer_appointment_slider_widget.dart b/lib/views/appointments/widgets/customer_appointment_slider_widget.dart index 7cfb819..afcb0e5 100644 --- a/lib/views/appointments/widgets/customer_appointment_slider_widget.dart +++ b/lib/views/appointments/widgets/customer_appointment_slider_widget.dart @@ -1,3 +1,4 @@ +import 'package:car_customer_app/view_models/dashboard_view_model.dart'; import 'package:carousel_slider/carousel_slider.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; @@ -8,7 +9,9 @@ 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/theme/colors.dart'; import 'package:mc_common_app/utils/navigator.dart'; +import 'package:mc_common_app/views/advertisement/custom_add_button.dart'; import 'package:mc_common_app/widgets/extensions/extensions_widget.dart'; +import 'package:provider/provider.dart'; class CustomerAppointmentSliderWidget extends StatelessWidget { final List myUpComingAppointments; @@ -18,31 +21,41 @@ class CustomerAppointmentSliderWidget extends StatelessWidget { @override Widget build(BuildContext context) { if (myUpComingAppointments.isEmpty) { - return InkWell( - child: Container( - height: 86, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - height: 24, - width: 24, - decoration: BoxDecoration(shape: BoxShape.circle, color: MyColors.darkTextColor), - child: Icon( - Icons.add, - color: MyColors.white, - ), - ), - SizedBox(width: 10), - "Add New Appointment".toText( - fontSize: 15, - isBold: true, - color: MyColors.lightTextColor, + return CustomAddButton( + needsBorder: true, + bgColor: MyColors.white, + onTap: () => context.read().onNavbarTapped(1), + text: "Add New Appointment", + icon: Container( + height: 24, + width: 24, + decoration: const BoxDecoration(shape: BoxShape.circle, color: MyColors.darkTextColor), + child: const Icon(Icons.add, color: MyColors.white), + ), + ).padding(EdgeInsets.symmetric(vertical: 10, horizontal: 21)); + return Container( + height: 86, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + height: 24, + width: 24, + decoration: BoxDecoration(shape: BoxShape.circle, color: MyColors.darkTextColor), + child: Icon( + Icons.add, + color: MyColors.white, ), - ], - ), + ), + SizedBox(width: 10), + "Add New Appointment".toText( + fontSize: 15, + isBold: true, + color: MyColors.lightTextColor, + ), + ], ), - ).toWhiteContainer(width: double.infinity, margin: EdgeInsets.symmetric(horizontal: 21, vertical: 10)); + ).onPress(() {}).toWhiteContainer(width: double.infinity, margin: EdgeInsets.symmetric(horizontal: 21, vertical: 10)); } return CarouselSlider.builder( options: CarouselOptions( @@ -87,14 +100,14 @@ class BuildAppointmentContainerForCustomer extends StatelessWidget { } List buildServicesFromAppointment({required AppointmentListModel appointmentListModel}) { - if (appointmentListModel.serviceAppointmentItems == null || appointmentListModel.serviceAppointmentItems!.isEmpty) { + if (appointmentListModel.appointmentServicesList == null || appointmentListModel.appointmentServicesList!.isEmpty) { return [SizedBox()]; } - if (appointmentListModel.serviceAppointmentItems!.length == 1) { + if (appointmentListModel.appointmentServicesList!.length == 1) { return [ showServices( - appointmentListModel.serviceAppointmentItems![0].serviceItemName!, + appointmentListModel.appointmentServicesList![0].providerServiceDescription, MyAssets.modificationsIcon, ) ]; @@ -102,16 +115,13 @@ class BuildAppointmentContainerForCustomer extends StatelessWidget { List servicesList = List.generate( 2, - (index) => showServices( - appointmentListModel.serviceAppointmentItems![index].serviceItemName!, - MyAssets.modificationsIcon, - ), + (index) => showServices(appointmentListModel.appointmentServicesList![index].providerServiceDescription, MyAssets.modificationsIcon), ); - if (appointmentListModel.serviceAppointmentItems!.length > 1) { + if (appointmentListModel.appointmentServicesList!.length > 1) { servicesList.add( showServices( - "+ ${appointmentListModel.serviceAppointmentItems!.length - 1} More", + "+ ${appointmentListModel.appointmentServicesList!.length - 1} More", "", isMoreText: true, ), diff --git a/lib/views/dashboard/dashboard_page.dart b/lib/views/dashboard/dashboard_page.dart index 7145d88..83ca892 100644 --- a/lib/views/dashboard/dashboard_page.dart +++ b/lib/views/dashboard/dashboard_page.dart @@ -34,30 +34,30 @@ class _DashboardPageState extends State { void initState() { super.initState(); // dashboardVM = Provider.of(context, listen: false); - fetchUsername(); scheduleMicrotask(() { context.read().populateAppointmentsFilterList(); - context.read().populateProvidersFilterList(); + context.read().populateBranchesFilterList(); context.read().populateAdsFilterList(); - context.read().getAllNearBranches(); _onRefresh(); }); } Future _onRefresh() async { - AdVM adVm = Provider.of(context, listen: false); + AdVM adVM = Provider.of(context, listen: false); AppointmentsVM appointmentsVM = Provider.of(context, listen: false); if (appointmentsVM.myAppointments.isEmpty) { await appointmentsVM.getMyAppointments(); } - if (adVm.myAds.isEmpty) { - await adVm.getMyAds(); + if (appointmentsVM.nearbyBranches.isEmpty) { + await appointmentsVM.getAllNearBranches(); } - if (adVm.exploreAds.isEmpty) { - await adVm.getExploreAds(); + if (adVM.myAds.isEmpty) { + await adVM.getMyAds(); + } + if (adVM.exploreAds.isEmpty) { + await adVM.getExploreAds(); } - final adVM = context.read(); if (adVM.vehicleTypes.isEmpty) { await adVM.getVehicleTypes(); } @@ -74,8 +74,6 @@ class _DashboardPageState extends State { ); } - fetchUsername() async {} - List fragments = [ const BranchesFragment(), const AppointmentsFragment(), @@ -106,7 +104,6 @@ class _DashboardPageState extends State { @override Widget build(BuildContext context) { bool isHomePage = context.watch().selectedNavbarBarIndex == 2; - bool isAdsPage = context.watch().selectedNavbarBarIndex == 3; return Scaffold( appBar: CustomAppBar( backgroundColor: null, diff --git a/lib/views/dashboard/fragments/appointments_fragment.dart b/lib/views/dashboard/fragments/appointments_fragment.dart index aff1704..deefa02 100644 --- a/lib/views/dashboard/fragments/appointments_fragment.dart +++ b/lib/views/dashboard/fragments/appointments_fragment.dart @@ -25,11 +25,11 @@ class AppointmentsFragment extends StatelessWidget { 16.height, FiltersList( filterList: appointmentsVM.appointmentsFilterOptions, - onFilterTapped: (index, selectedFilterId) => appointmentsVM.applyFilterOnAppointmentsVM(index: index), + onFilterTapped: (index, selectedFilterId) => appointmentsVM.applyFilterOnAppointmentsVM(appointmentStatusEnum: selectedFilterId.toAppointmentStatusEnum()), ), 16.height, Expanded( - child: appointmentsVM.myAppointments.isEmpty + child: appointmentsVM.myFilteredAppointments.isEmpty ? Column( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -38,15 +38,15 @@ class AppointmentsFragment extends StatelessWidget { ) : ListView.builder( shrinkWrap: true, - itemCount: appointmentsVM.myAppointments.length, + itemCount: appointmentsVM.myFilteredAppointments.length, itemBuilder: (BuildContext context, int index) { return BuildAppointmentContainerForCustomer( onTapped: () => navigateWithName( context, AppRoutes.appointmentDetailView, - arguments: appointmentsVM.myAppointments[index], + arguments: appointmentsVM.myFilteredAppointments[index], ), - appointmentListModel: appointmentsVM.myAppointments[index], + appointmentListModel: appointmentsVM.myFilteredAppointments[index], ); }, ), diff --git a/lib/views/dashboard/fragments/home_fragment.dart b/lib/views/dashboard/fragments/home_fragment.dart index 6a219cc..8690b6d 100644 --- a/lib/views/dashboard/fragments/home_fragment.dart +++ b/lib/views/dashboard/fragments/home_fragment.dart @@ -32,20 +32,21 @@ class HomeFragment extends StatelessWidget { subTitle: "View All", onSubtitleTapped: () { context.read().onNavbarTapped(1); - context.read().applyFilterOnAppointmentsVM(index: 0); + context.read().applyFilterOnAppointmentsVM(appointmentStatusEnum: AppointmentStatusEnum.allAppointments); }, ).horPaddingMain(), - CustomerAppointmentSliderWidget(myUpComingAppointments: context.watch().myAppointments), + CustomerAppointmentSliderWidget(myUpComingAppointments: context.read().myUpComingAppointments), 7.height, ViewAllWidget( title: "My Recent Service Providers".toUpperCase(), subTitle: "View All", onSubtitleTapped: () { - print("value: ${AppState().getUser.data!.accessToken}"); context.read().onNavbarTapped(0); context.read().applyFilterOnProviders(index: 0); }).horPaddingMain(), - const ServiceProviderWidget().horPaddingMain(), + Consumer(builder: (BuildContext context, AppointmentsVM appointmentsVM, Widget? child) { + return ServiceProviderWidget(nearbyBranches: context.read().nearbyBranches).horPaddingMain(); + }), Consumer( builder: (BuildContext context, AdVM adVM, Widget? child) { return Column( diff --git a/lib/views/provider/branch_detail_page.dart b/lib/views/provider/branch_detail_page.dart index 0f26312..268ff39 100644 --- a/lib/views/provider/branch_detail_page.dart +++ b/lib/views/provider/branch_detail_page.dart @@ -94,13 +94,7 @@ class _BranchDetailPageState extends State { color: MyColors.primaryColor, ), ], - ) - .padding( - EdgeInsets.symmetric(vertical: 6, horizontal: 2), - ) - .onPress( - () {}, - ), + ).padding(EdgeInsets.symmetric(vertical: 6, horizontal: 2)), Row( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, @@ -119,14 +113,8 @@ class _BranchDetailPageState extends State { ), ], ) - .padding( - EdgeInsets.symmetric(vertical: 6, horizontal: 2), - ) - .onPress( - () { - navigateWithName(context, AppRoutes.providerProfilePage, arguments: widget.branchDetailModel.serviceProviderId); - }, - ), + .padding(EdgeInsets.symmetric(vertical: 6, horizontal: 2)) + .onPress(() => navigateWithName(context, AppRoutes.providerProfilePage, arguments: widget.branchDetailModel.serviceProviderId)), ], ), 20.height, From 937a1db5d9265c4d081fef2f65c6a90a0b3a1fb6 Mon Sep 17 00:00:00 2001 From: Faiz Hashmi Date: Tue, 31 Oct 2023 12:27:15 +0300 Subject: [PATCH 4/5] Completed the Appointment Payments --- lib/config/customer_routes.dart | 4 +- lib/view_models/appointments_view_model.dart | 71 ++++++++++++++++++- .../appointments/appointment_detail_view.dart | 49 ++++++++----- .../book_appointment_schedules_view.dart | 23 +++++- .../fragments/appointments_fragment.dart | 19 +++-- .../dashboard/fragments/home_fragment.dart | 1 - lib/views/provider/branch_detail_page.dart | 4 ++ 7 files changed, 143 insertions(+), 28 deletions(-) diff --git a/lib/config/customer_routes.dart b/lib/config/customer_routes.dart index 5075082..1b55355 100644 --- a/lib/config/customer_routes.dart +++ b/lib/config/customer_routes.dart @@ -29,7 +29,9 @@ class CustomerAppRoutes { AppRoutes.adsSearchFilterScreen: (context) => AdsSearchFilterView(), AppRoutes.selectAdTypeView: (context) => SelectAdTypeView(isProvider: ModalRoute.of(context)!.settings.arguments as bool), AppRoutes.bookAppointmenServicesView: (context) => BookAppointmentServicesView(), - AppRoutes.bookAppointmenSchedulesView: (context) => BookAppointmentSchedulesView(), + AppRoutes.bookAppointmenSchedulesView: (context) => BookAppointmentSchedulesView( + screenArgumentsForAppointmentDetailPage: ModalRoute.of(context)!.settings.arguments as ScreenArgumentsForAppointmentDetailPage, + ), AppRoutes.bookAppointmentsItemView: (context) => BookAppointmentsItemView(), AppRoutes.reviewAppointmentView: (context) => ReviewAppointment(), AppRoutes.paymentMethodsView: (context) => PaymentMethodsView(paymentType: ModalRoute.of(context)!.settings.arguments as PaymentTypes), diff --git a/lib/view_models/appointments_view_model.dart b/lib/view_models/appointments_view_model.dart index d87f3be..ce6a26b 100644 --- a/lib/view_models/appointments_view_model.dart +++ b/lib/view_models/appointments_view_model.dart @@ -1,6 +1,7 @@ import 'package:car_customer_app/repositories/provider_repo.dart'; import 'package:car_customer_app/repositories/appointment_repo.dart'; import 'package:car_customer_app/view_models/dashboard_view_model.dart'; +import 'package:car_customer_app/views/appointments/book_appointment_schedules_view.dart'; import 'package:car_customer_app/views/appointments/widgets/appointment_service_pick_bottom_sheet.dart'; import 'package:flutter/material.dart'; import 'package:mc_common_app/classes/consts.dart'; @@ -127,6 +128,11 @@ class AppointmentsVM extends BaseVM { } } + 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 { @@ -209,6 +215,7 @@ class AppointmentsVM extends BaseVM { resetAfterBookingAppointment() { servicesInCurrentAppointment.clear(); allSelectedItemsInAppointments.clear(); + serviceAppointmentScheduleList.clear(); } populateAppointmentsFilterList() { @@ -597,8 +604,70 @@ class AppointmentsVM extends BaseVM { ); Utils.hideLoading(context); - navigateWithName(context, AppRoutes.bookAppointmenSchedulesView); + 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()}"); + } + } } diff --git a/lib/views/appointments/appointment_detail_view.dart b/lib/views/appointments/appointment_detail_view.dart index 5b8d01d..ac79906 100644 --- a/lib/views/appointments/appointment_detail_view.dart +++ b/lib/views/appointments/appointment_detail_view.dart @@ -19,13 +19,6 @@ class AppointmentDetailView extends StatelessWidget { AppointmentDetailView({Key? key, required this.appointmentListModel}) : super(key: key); - final List servicesList = [ - "Mechanic", - "Electrician", - " Car Denting", - "Oil Change", - ]; - Widget getBaseActionButtonWidget({required Color color, required String text, Color textColor = MyColors.white, required Function() onPressed}) { return Expanded( child: ShowFillButton( @@ -48,7 +41,12 @@ class AppointmentDetailView extends StatelessWidget { children: [ getBaseActionButtonWidget(color: MyColors.redColor, onPressed: () => appointmentCancelConfirmationSheet(context), text: "Cancel"), 12.width, - getBaseActionButtonWidget(color: MyColors.greenColor, onPressed: () {}, text: "Confirm"), + getBaseActionButtonWidget( + color: MyColors.greenColor, + onPressed: () { + context.read().onConfirmAppointmentPressed(context: context, appointmentId: appointmentListModel.id); + }, + text: "Confirm"), ], ), ); @@ -119,6 +117,7 @@ class AppointmentDetailView extends StatelessWidget { @override Widget build(BuildContext context) { + AppointmentsVM appointmentsVM = context.read(); return Scaffold( appBar: CustomAppBar( title: "Appointment", @@ -134,16 +133,16 @@ class AppointmentDetailView extends StatelessWidget { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - appointmentListModel.providerName!.toText(fontSize: 18, isBold: true), + appointmentListModel.providerName!.toText(fontSize: 16, isBold: true), Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ MyAssets.miniClockDark.buildSvg( - height: 10, - width: 10, + height: 12, + width: 12, fit: BoxFit.fill, ), - 10.width, + 5.width, "${appointmentListModel.duration ?? ""} ${appointmentListModel.appointmentDate!.toFormattedDateWithoutTime()}".toText(fontSize: 12, isBold: true, color: MyColors.lightTextColor), ], ), @@ -163,7 +162,7 @@ class AppointmentDetailView extends StatelessWidget { // fit: BoxFit.fill, // ), // 10.width, - "${index + 1}. ${service.providerServiceDescription}".toText(fontSize: 18, isBold: true), + "${index + 1}. ${service.providerServiceDescription}".toText(fontSize: 14, isBold: true), ], ), if (service.serviceItems != null && service.serviceItems!.isNotEmpty) ...[ @@ -172,13 +171,25 @@ class AppointmentDetailView extends StatelessWidget { service.serviceItems!.length, (index) => "${service.serviceItems![index].name}".toText( textAlign: TextAlign.start, - fontSize: 13, - isBold: true, + fontSize: 12, color: MyColors.lightTextColor, ), ), ).paddingOnly(left: 15), ], + 5.height, + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + ((service.currentTotalServicePrice).toString()).toText(fontSize: 25, isBold: true), + 2.width, + "SAR".toText(color: MyColors.lightTextColor, fontSize: 16, isBold: true).paddingOnly(bottom: 5), + Icon( + Icons.arrow_drop_down, + size: 30, + ) + ], + ).onPress(() => appointmentsVM.priceBreakDownClicked(context, service)), ], ); }), @@ -189,14 +200,18 @@ class AppointmentDetailView extends StatelessWidget { children: [ CardButtonWithIcon( title: "Reschedule Appointment", - onCardTapped: () {}, + onCardTapped: () { + context.read().onRescheduleAppointmentPressed(context: context, appointmentListModel: appointmentListModel); + }, icon: MyAssets.scheduleAppointmentIcon.buildSvg(), ), if (appointmentListModel.appointmentStatusEnum == AppointmentStatusEnum.booked) ...[ 10.width, CardButtonWithIcon( title: "Pay for Appointment", - onCardTapped: () {}, + onCardTapped: () { + context.read().onConfirmAppointmentPressed(context: context, appointmentId: appointmentListModel.id); + }, icon: MyAssets.creditCardIcon.buildSvg(), ), ], diff --git a/lib/views/appointments/book_appointment_schedules_view.dart b/lib/views/appointments/book_appointment_schedules_view.dart index d6614d7..9cccde3 100644 --- a/lib/views/appointments/book_appointment_schedules_view.dart +++ b/lib/views/appointments/book_appointment_schedules_view.dart @@ -13,8 +13,17 @@ import 'package:mc_common_app/widgets/common_widgets/time_slots.dart'; import 'package:mc_common_app/widgets/extensions/extensions_widget.dart'; import 'package:provider/provider.dart'; +class ScreenArgumentsForAppointmentDetailPage { + final int routeFlag; // 1 = coming from create appointment || 2 = coming from reschedule appointment + final int appointmentId; // 1 = coming from create appointment || 2 = coming from reschedule appointment + + ScreenArgumentsForAppointmentDetailPage({required this.routeFlag, required this.appointmentId}); +} + class BookAppointmentSchedulesView extends StatelessWidget { - BookAppointmentSchedulesView({Key? key}) : super(key: key); + final ScreenArgumentsForAppointmentDetailPage screenArgumentsForAppointmentDetailPage; + + BookAppointmentSchedulesView({Key? key, required this.screenArgumentsForAppointmentDetailPage}) : super(key: key); @override Widget build(BuildContext context) { @@ -125,9 +134,17 @@ class BookAppointmentSchedulesView extends StatelessWidget { Expanded( child: ShowFillButton( maxHeight: 55, - title: "Review", + title: screenArgumentsForAppointmentDetailPage.routeFlag == 1 ? "Review" : "Confirm", onPressed: () { - appointmentsVM.onReviewButtonPressed(context); + if (screenArgumentsForAppointmentDetailPage.routeFlag == 1) { + appointmentsVM.onReviewButtonPressed(context); + } else { + appointmentsVM.onRescheduleAppointmentConfirmPressed( + context: context, + appointmentId: screenArgumentsForAppointmentDetailPage.appointmentId, + selectedSlotId: appointmentsVM.serviceAppointmentScheduleList.first.selectedCustomTimeDateSlotModel!.date!.slotId, + ); + } }, backgroundColor: MyColors.darkPrimaryColor, ), diff --git a/lib/views/dashboard/fragments/appointments_fragment.dart b/lib/views/dashboard/fragments/appointments_fragment.dart index deefa02..382d923 100644 --- a/lib/views/dashboard/fragments/appointments_fragment.dart +++ b/lib/views/dashboard/fragments/appointments_fragment.dart @@ -1,9 +1,12 @@ +import 'dart:developer'; + import 'package:car_customer_app/view_models/appointments_view_model.dart'; import 'package:car_customer_app/views/appointments/widgets/customer_appointment_slider_widget.dart'; import 'package:flutter/material.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/theme/colors.dart'; import 'package:mc_common_app/utils/navigator.dart'; import 'package:mc_common_app/widgets/common_widgets/categories_list.dart'; @@ -41,11 +44,17 @@ class AppointmentsFragment extends StatelessWidget { itemCount: appointmentsVM.myFilteredAppointments.length, itemBuilder: (BuildContext context, int index) { return BuildAppointmentContainerForCustomer( - onTapped: () => navigateWithName( - context, - AppRoutes.appointmentDetailView, - arguments: appointmentsVM.myFilteredAppointments[index], - ), + onTapped: () { + AppointmentListModel appointmentModel = appointmentsVM.myFilteredAppointments[index]; + appointmentModel.appointmentServicesList!.forEach((service) { + double totalServicePrice = 0.0; + service.serviceItems!.forEach((item) { + totalServicePrice = totalServicePrice + (double.parse("${item.price ?? 0.0}")); + }); + service.currentTotalServicePrice = totalServicePrice; + }); + navigateWithName(context, AppRoutes.appointmentDetailView, arguments: appointmentModel); + }, appointmentListModel: appointmentsVM.myFilteredAppointments[index], ); }, diff --git a/lib/views/dashboard/fragments/home_fragment.dart b/lib/views/dashboard/fragments/home_fragment.dart index 8690b6d..cf384cd 100644 --- a/lib/views/dashboard/fragments/home_fragment.dart +++ b/lib/views/dashboard/fragments/home_fragment.dart @@ -2,7 +2,6 @@ import 'package:car_customer_app/view_models/appointments_view_model.dart'; import 'package:car_customer_app/view_models/dashboard_view_model.dart'; import 'package:car_customer_app/views/appointments/widgets/customer_appointment_slider_widget.dart'; import 'package:flutter/material.dart'; -import 'package:mc_common_app/classes/app_state.dart'; import 'package:mc_common_app/extensions/int_extensions.dart'; import 'package:mc_common_app/utils/enums.dart'; import 'package:mc_common_app/view_models/ad_view_model.dart'; diff --git a/lib/views/provider/branch_detail_page.dart b/lib/views/provider/branch_detail_page.dart index 268ff39..142aee8 100644 --- a/lib/views/provider/branch_detail_page.dart +++ b/lib/views/provider/branch_detail_page.dart @@ -38,6 +38,10 @@ class _BranchDetailPageState extends State { return Scaffold( appBar: CustomAppBar( title: "Branch Detail", + onBackButtonTapped: () { + context.read().resetCategorySelectionBottomSheet(); + Navigator.pop(context); + }, ), body: Column( children: [ From 9638deea3d8accc96a33a837e7aae7c26ed65f5a Mon Sep 17 00:00:00 2001 From: Faiz Hashmi Date: Thu, 2 Nov 2023 11:19:50 +0300 Subject: [PATCH 5/5] Completed the Appointment Payments --- lib/config/customer_routes.dart | 6 +++--- lib/views/dashboard/dashboard_page.dart | 7 ++++++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/config/customer_routes.dart b/lib/config/customer_routes.dart index 1b55355..2296dc4 100644 --- a/lib/config/customer_routes.dart +++ b/lib/config/customer_routes.dart @@ -17,16 +17,16 @@ import 'package:mc_common_app/views/advertisement/ads_detail_view.dart'; import 'package:mc_common_app/views/advertisement/create_ad_view.dart'; import 'package:mc_common_app/views/advertisement/select_ad_type_view.dart'; import 'package:mc_common_app/views/payments/payment_methods_view.dart'; -import 'package:mc_common_app/views/advertisement/ads_search_filter_view.dart'; +import 'package:mc_common_app/views/advertisement/ads_filter_view.dart'; -class CustomerAppRoutes { +class CustomerAppRoutes { static final Map routes = { AppRoutes.dashboard: (context) => DashboardPage(), AppRoutes.bookProviderAppView: (context) => BookProviderAppView(), AppRoutes.appointmentDetailView: (context) => AppointmentDetailView(appointmentListModel: ModalRoute.of(context)!.settings.arguments as AppointmentListModel), AppRoutes.adsDetailView: (context) => AdsDetailView(adDetails: ModalRoute.of(context)!.settings.arguments as AdDetailsModel), AppRoutes.createAdView: (context) => CreateAdView(), - AppRoutes.adsSearchFilterScreen: (context) => AdsSearchFilterView(), + AppRoutes.adsFilterView: (context) => AdsFilterView(), AppRoutes.selectAdTypeView: (context) => SelectAdTypeView(isProvider: ModalRoute.of(context)!.settings.arguments as bool), AppRoutes.bookAppointmenServicesView: (context) => BookAppointmentServicesView(), AppRoutes.bookAppointmenSchedulesView: (context) => BookAppointmentSchedulesView( diff --git a/lib/views/dashboard/dashboard_page.dart b/lib/views/dashboard/dashboard_page.dart index 83ca892..f0d4b5e 100644 --- a/lib/views/dashboard/dashboard_page.dart +++ b/lib/views/dashboard/dashboard_page.dart @@ -116,7 +116,12 @@ class _DashboardPageState extends State { (isHomePage ? MyAssets.notificationsBellIcon.buildSvg() : InkWell( - onTap: () => navigateWithName(context, AppRoutes.adsSearchFilterScreen), + onTap: () async { + if (context.read().selectedNavbarBarIndex == 3) { + await context.read().populateDataForAdFilter(); + navigateWithName(context, AppRoutes.adsFilterView); + } + }, child: MyAssets.searchIcon.buildSvg(), )) .paddingOnly(right: 21)